Create backup folder in the directory where 'dotnet migrate' is executed (#5306)

* Create backup folder in the directory where 'dotnet migrate' is executed

With this change, 'dotnet migrate' will create the backup folder in the workspace directory rather
than the parent of the workspace directory. This solves two problems:

1. It makes it easier for the user where the backup is -- it's in the directory they targeted with
'dotnet migrate'.
2. It solves a problem of file oollisions with global.json files when migrating multiple projects.
Consider the following directory structure:

    root
        |
        project1
            |
            global.json
            |
            src
                |
                project1
        project2
            |
            global.json
            |
            src
                |
                project2

    Prior to this change, running 'dotnet migrate' project1 and then running it again in project2
    would have caused an exception to be thrown because the migration would try to produce a backup
    folder like so:

    root
        |
        backup
        |  |
        |   global.json
        |   |
        |   project1
        |   |
        |   project2
        |
        |
        project1
            |
            src
                |
                project1
        project2
            |
            src
                |
                project2

    Now, we produce the following structure, which has no collisions:

    root
        |
        project1
            |
            backup
            |   |
            |   global.json
            |   |
            |   project1
            |
            src
                |
                project1
        |
        project2
            |
            backup
            |   |
            |   global.json
            |   |
            |   project2
            |
            src
                |
                project2

In addition, to help avoid further collisions, a number is appened to the backup folder's name if
it already exists. So, if the user runs dotnet migrate again for some reason, they'll see backup_1,
backup_2, etc.

* Fix test helper

* Fix foolish bug causing infinite loop

* Fix up a couple more tests

* Rework MigrationBackupPlan to process all projects at once

* Fix up tests

* Still fixing tests

* Compute common root folder of projects to determine where backup folder should be placed

* Fix typo

* Fix test to not look in backup folder now that it's in a better location
This commit is contained in:
Dustin Campbell 2017-01-21 01:58:28 -08:00 committed by Piotr Puszkiewicz
parent 04a7fca9fc
commit 0a62481cc0
6 changed files with 267 additions and 104 deletions

View file

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.DotNet.Internal.ProjectModel.Utilities;
namespace Microsoft.DotNet.ProjectJsonMigration
@ -14,92 +15,206 @@ namespace Microsoft.DotNet.ProjectJsonMigration
private const string TempCsprojExtention = ".migration_in_place_backup";
private readonly FileInfo globalJson;
private readonly Dictionary<DirectoryInfo, IEnumerable<FileInfo>> mapOfProjectBackupDirectoryToFilesToMove;
public DirectoryInfo RootBackupDirectory { get; }
public DirectoryInfo[] ProjectBackupDirectories { get; }
public IEnumerable<FileInfo> FilesToMove(DirectoryInfo projectBackupDirectory)
=> mapOfProjectBackupDirectoryToFilesToMove[projectBackupDirectory];
public MigrationBackupPlan(
DirectoryInfo projectDirectory,
IEnumerable<DirectoryInfo> projectDirectories,
DirectoryInfo workspaceDirectory,
Func<DirectoryInfo, IEnumerable<FileInfo>> getFiles = null)
{
if (projectDirectory == null)
if (projectDirectories == null)
{
throw new ArgumentNullException(nameof(projectDirectory));
throw new ArgumentNullException(nameof(projectDirectories));
}
if (!projectDirectories.Any())
{
throw new ArgumentException("No project directories provided.", nameof(projectDirectories));
}
if (workspaceDirectory == null)
{
throw new ArgumentNullException(nameof(workspaceDirectory));
}
globalJson = new FileInfo(Path.Combine(
workspaceDirectory.FullName,
"global.json"));
MigrationTrace.Instance.WriteLine("Computing migration backup plan...");
projectDirectory = new DirectoryInfo(projectDirectory.FullName.EnsureTrailingSlash());
projectDirectories = projectDirectories.Select(pd => new DirectoryInfo(pd.FullName.EnsureTrailingSlash()));
workspaceDirectory = new DirectoryInfo(workspaceDirectory.FullName.EnsureTrailingSlash());
MigrationTrace.Instance.WriteLine($" Workspace: {workspaceDirectory.FullName}");
foreach (var projectDirectory in projectDirectories)
{
MigrationTrace.Instance.WriteLine($" Project: {projectDirectory.FullName}");
}
var rootDirectory = FindCommonRootPath(projectDirectories.ToArray()) ?? workspaceDirectory;
rootDirectory = new DirectoryInfo(rootDirectory.FullName.EnsureTrailingSlash());
MigrationTrace.Instance.WriteLine($" Root: {rootDirectory.FullName}");
globalJson = new FileInfo(
Path.Combine(
workspaceDirectory.FullName,
"global.json"));
RootBackupDirectory = new DirectoryInfo(
Path.Combine(
workspaceDirectory.Parent.FullName,
"backup")
GetUniqueDirectoryPath(
Path.Combine(
rootDirectory.FullName,
"backup"))
.EnsureTrailingSlash());
ProjectBackupDirectory = new DirectoryInfo(
Path.Combine(
RootBackupDirectory.FullName,
projectDirectory.Name)
.EnsureTrailingSlash());
MigrationTrace.Instance.WriteLine($" Root Backup: {RootBackupDirectory.FullName}");
var relativeDirectory = PathUtility.GetRelativePath(
workspaceDirectory.FullName,
projectDirectory.FullName);
var projectBackupDirectories = new List<DirectoryInfo>();
mapOfProjectBackupDirectoryToFilesToMove = new Dictionary<DirectoryInfo, IEnumerable<FileInfo>>();
getFiles = getFiles ?? (dir => dir.EnumerateFiles());
getFiles = getFiles ??
(dir => dir.EnumerateFiles());
foreach (var projectDirectory in projectDirectories)
{
var projectBackupDirectory = ComputeProjectBackupDirectoryPath(rootDirectory, projectDirectory, RootBackupDirectory);
var filesToMove = getFiles(projectDirectory).Where(NeedsBackup);
FilesToMove = getFiles(projectDirectory)
.Where(f => f.Name == "project.json"
|| f.Extension == ".xproj"
|| f.FullName.EndsWith(".xproj.user")
|| f.FullName.EndsWith(".lock.json")
|| f.FullName.EndsWith(TempCsprojExtention));
projectBackupDirectories.Add(projectBackupDirectory);
mapOfProjectBackupDirectoryToFilesToMove.Add(projectBackupDirectory, filesToMove);
}
ProjectBackupDirectories = projectBackupDirectories.ToArray();
}
public DirectoryInfo ProjectBackupDirectory { get; }
public DirectoryInfo RootBackupDirectory { get; }
public IEnumerable<FileInfo> FilesToMove { get; }
public void PerformBackup()
{
if (globalJson.Exists)
{
PathUtility.EnsureDirectoryExists(RootBackupDirectory.FullName);
globalJson.MoveTo(Path.Combine(
ProjectBackupDirectory.Parent.FullName,
globalJson.Name));
globalJson.MoveTo(
Path.Combine(
RootBackupDirectory.FullName,
globalJson.Name));
}
PathUtility.EnsureDirectoryExists(ProjectBackupDirectory.FullName);
foreach (var file in FilesToMove)
foreach (var kvp in mapOfProjectBackupDirectoryToFilesToMove)
{
var fileName = file.Name.EndsWith(TempCsprojExtention)
? Path.GetFileNameWithoutExtension(file.Name)
: file.Name;
var projectBackupDirectory = kvp.Key;
var filesToMove = kvp.Value;
file.MoveTo(
Path.Combine(ProjectBackupDirectory.FullName, fileName));
PathUtility.EnsureDirectoryExists(projectBackupDirectory.FullName);
foreach (var file in filesToMove)
{
var fileName = file.Name.EndsWith(TempCsprojExtention)
? Path.GetFileNameWithoutExtension(file.Name)
: file.Name;
file.MoveTo(
Path.Combine(
projectBackupDirectory.FullName,
fileName));
}
}
}
private static DirectoryInfo ComputeProjectBackupDirectoryPath(
DirectoryInfo rootDirectory, DirectoryInfo projectDirectory, DirectoryInfo rootBackupDirectory)
{
if (PathUtility.IsChildOfDirectory(rootDirectory.FullName, projectDirectory.FullName))
{
var relativePath = PathUtility.GetRelativePath(
rootDirectory.FullName,
projectDirectory.FullName);
return new DirectoryInfo(
Path.Combine(
rootBackupDirectory.FullName,
relativePath)
.EnsureTrailingSlash());
}
// Ensure that we use a unique name to avoid collisions as a fallback.
return new DirectoryInfo(
GetUniqueDirectoryPath(
Path.Combine(
rootBackupDirectory.FullName,
projectDirectory.Name)
.EnsureTrailingSlash()));
}
private static bool NeedsBackup(FileInfo file)
=> file.Name == "project.json"
|| file.Extension == ".xproj"
|| file.FullName.EndsWith(".xproj.user")
|| file.FullName.EndsWith(".lock.json")
|| file.FullName.EndsWith(TempCsprojExtention);
private static string GetUniqueDirectoryPath(string directoryPath)
{
var candidatePath = directoryPath;
var suffix = 1;
while (Directory.Exists(candidatePath))
{
candidatePath = $"{directoryPath}_{suffix++}";
}
return candidatePath;
}
private static DirectoryInfo FindCommonRootPath(DirectoryInfo[] paths)
{
var pathSplits = new string[paths.Length][];
var shortestLength = int.MaxValue;
for (int i = 0; i < paths.Length; i++)
{
pathSplits[i] = paths[i].FullName.Split(new[] { Path.DirectorySeparatorChar });
shortestLength = Math.Min(shortestLength, pathSplits[i].Length);
}
var builder = new StringBuilder();
var splitIndex = 0;
while (splitIndex < shortestLength)
{
var split = pathSplits[0][splitIndex];
var done = false;
for (int i = 1; i < pathSplits.Length; i++)
{
if (pathSplits[i][splitIndex] != split)
{
done = true;
break;
}
}
if (done)
{
break;
}
builder.Append(split);
builder.Append(Path.DirectorySeparatorChar);
splitIndex++;
}
return new DirectoryInfo(builder.ToString().EnsureTrailingSlash());
}
public static void RenameCsprojFromMigrationOutputNameToTempName(string outputProject)
{
var backupFileName = $"{outputProject}{TempCsprojExtention}";
if (File.Exists(backupFileName))
{
File.Delete(backupFileName);
}
File.Move(outputProject, backupFileName);
}
}

View file

@ -30,7 +30,7 @@ namespace Microsoft.DotNet.TestFramework
var testDirectory = new DirectoryInfo(path);
var migrationBackupDirectory = new DirectoryInfo(
System.IO.Path.Combine(testDirectory.Parent.FullName, "backup"));
System.IO.Path.Combine(testDirectory.FullName, "backup"));
if (testDirectory.Exists)
{

View file

@ -202,14 +202,19 @@ namespace Microsoft.DotNet.Tools.Migrate
private void BackupProjects(MigrationReport migrationReport)
{
var projectDirectories = new List<DirectoryInfo>();
foreach (var report in migrationReport.ProjectMigrationReports)
{
var backupPlan = new MigrationBackupPlan(
new DirectoryInfo(report.ProjectDirectory),
_workspaceDirectory);
backupPlan.PerformBackup();
projectDirectories.Add(new DirectoryInfo(report.ProjectDirectory));
}
var backupPlan = new MigrationBackupPlan(
projectDirectories,
_workspaceDirectory);
backupPlan.PerformBackup();
Reporter.Output.WriteLine($"Files backed up to {backupPlan.RootBackupDirectory.FullName}");
}
private void WriteReport(MigrationReport migrationReport)

View file

@ -5,6 +5,7 @@ using FluentAssertions;
using Microsoft.DotNet.Internal.ProjectModel.Utilities;
using Microsoft.DotNet.ProjectJsonMigration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Xunit;
@ -14,81 +15,123 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests
public partial class MigrationBackupPlanTests
{
[Fact]
public void TheRootBackupDirectoryIsASiblingOfTheRootProject()
public void TheBackupDirectoryIsASubfolderOfTheMigratedProject()
{
var dir = new DirectoryInfo(Path.Combine("src", "some-proj"));
var workspaceDirectory = Path.Combine("src", "root");
var projectDirectory = Path.Combine("src", "project1");
System.Console.WriteLine(dir.FullName);
WhenMigrating(
projectDirectory: dir.FullName,
workspaceDirectory: Path.Combine("src", "RootProject"))
.RootBackupDirectory
.FullName
.Should()
.Be(new DirectoryInfo(Path.Combine("src", "backup")).FullName.EnsureTrailingSlash());
WhenMigrating(projectDirectory, workspaceDirectory)
.RootBackupDirectory
.FullName
.Should()
.Be(new DirectoryInfo(Path.Combine("src", "project1", "backup")).FullName.EnsureTrailingSlash());
}
[Fact]
public void TheRootProjectsBackupDirectoryIsASubfolderOfTheRootBackupDirectory()
public void TheBackupDirectoryIsASubfolderOfTheMigratedProjectWhenInitiatedFromProjectFolder()
{
WhenMigrating(
projectDirectory: Path.Combine("src", "RootProject"),
workspaceDirectory: Path.Combine("src", "RootProject"))
.ProjectBackupDirectory
.FullName
.Should()
.Be(new DirectoryInfo(Path.Combine("src", "backup", "RootProject")).FullName.EnsureTrailingSlash());
var workspaceDirectory = Path.Combine("src", "root");
var projectDirectory = Path.Combine("src", "root");
WhenMigrating(projectDirectory, workspaceDirectory)
.ProjectBackupDirectories.Single()
.FullName
.Should()
.Be(new DirectoryInfo(Path.Combine("src", "root", "backup")).FullName.EnsureTrailingSlash());
}
[Fact]
public void ADependentProjectsMigrationBackupDirectoryIsASubfolderOfTheRootBackupDirectory()
public void TheBackupDirectoryIsInTheCommonRootOfTwoProjectFoldersWhenInitiatedFromProjectFolder()
{
WhenMigrating(
projectDirectory: Path.Combine("src", "Dependency"),
workspaceDirectory: Path.Combine("src", "RootProject"))
.ProjectBackupDirectory
.FullName
.Should()
.Be(new DirectoryInfo(Path.Combine("src", "backup", "Dependency")).FullName.EnsureTrailingSlash());
var projectDirectories = new []
{
Path.Combine("root", "project1"),
Path.Combine("root", "project2")
};
var workspaceDirectory = Path.Combine("root", "project1");
WhenMigrating(projectDirectories, workspaceDirectory)
.RootBackupDirectory
.FullName
.Should()
.Be(new DirectoryInfo(Path.Combine("root", "backup")).FullName.EnsureTrailingSlash());
}
[Fact]
public void FilesToBackUpAreIdentifiedInTheTheRootProjectDirectory()
public void TheBackupDirectoryIsInTheCommonRootOfTwoProjectFoldersWhenInitiatedFromCommonRoot()
{
var root = new DirectoryInfo(Path.Combine("src", "RootProject"));
var projectDirectories = new []
{
Path.Combine("root", "project1"),
Path.Combine("root", "project2")
};
WhenMigrating(
projectDirectory: root.FullName,
workspaceDirectory: root.FullName)
.FilesToMove
.Should()
.Contain(_ => _.FullName == Path.Combine(root.FullName, "project.json"));
var workspaceDirectory = Path.Combine("root");
WhenMigrating(projectDirectories, workspaceDirectory)
.RootBackupDirectory
.FullName
.Should()
.Be(new DirectoryInfo(Path.Combine("root", "backup")).FullName.EnsureTrailingSlash());
}
[Fact]
public void FilesToBackUpAreIdentifiedInTheTheDependencyProjectDirectory()
public void TheBackupDirectoryIsInTheCommonRootOfTwoProjectFoldersAtDifferentLevelsWhenInitiatedFromProjectFolder()
{
var root = new DirectoryInfo(Path.Combine("src", "RootProject"));
var dependency = new DirectoryInfo(Path.Combine("src", "RootProject"));
var projectDirectories = new []
{
Path.Combine("root", "tests", "inner", "project1"),
Path.Combine("root", "src", "project2")
};
WhenMigrating(
projectDirectory: dependency.FullName,
workspaceDirectory: root.FullName)
.FilesToMove
.Should()
.Contain(_ => _.FullName == Path.Combine(dependency.FullName, "project.json"));
var workspaceDirectory = Path.Combine("root", "tests", "inner");
WhenMigrating(projectDirectories, workspaceDirectory)
.RootBackupDirectory
.FullName
.Should()
.Be(new DirectoryInfo(Path.Combine("root", "backup")).FullName.EnsureTrailingSlash());
}
private MigrationBackupPlan WhenMigrating(
string projectDirectory,
string workspaceDirectory) =>
[Fact]
public void FilesToBackUpAreIdentifiedInTheRootProjectDirectory()
{
var workspaceDirectory = Path.Combine("src", "root");
var projectDirectory = Path.Combine("src", "root");
var whenMigrating = WhenMigrating(projectDirectory, workspaceDirectory);
whenMigrating
.FilesToMove(whenMigrating.ProjectBackupDirectories.Single())
.Should()
.Contain(_ => _.FullName == Path.Combine(new DirectoryInfo(workspaceDirectory).FullName, "project.json"));
}
[Fact]
public void FilesToBackUpAreIdentifiedInTheDependencyProjectDirectory()
{
var workspaceDirectory = Path.Combine("src", "root");
var projectDirectory = Path.Combine("src", "root");
var whenMigrating = WhenMigrating(projectDirectory, workspaceDirectory);
whenMigrating
.FilesToMove(whenMigrating.ProjectBackupDirectories.Single())
.Should()
.Contain(_ => _.FullName == Path.Combine(new DirectoryInfo(projectDirectory).FullName, "project.json"));
}
private MigrationBackupPlan WhenMigrating(string projectDirectory, string workspaceDirectory) =>
new MigrationBackupPlan(
new DirectoryInfo(projectDirectory),
new [] { new DirectoryInfo(projectDirectory) },
new DirectoryInfo(workspaceDirectory),
dir => new[] { new FileInfo(Path.Combine(dir.FullName, "project.json")) });
dir => new [] { new FileInfo(Path.Combine(dir.FullName, "project.json")) });
private MigrationBackupPlan WhenMigrating(string[] projectDirectories, string workspaceDirectory) =>
new MigrationBackupPlan(
projectDirectories.Select(p => new DirectoryInfo(p)),
new DirectoryInfo(workspaceDirectory),
dir => new [] { new FileInfo(Path.Combine(dir.FullName, "project.json")) });
}
}

View file

@ -19,9 +19,7 @@ namespace Microsoft.DotNet.Migration.Tests
.CreateTestInstance(testProjectName)
.Path;
var testRootParent = new DirectoryInfo(testRoot).Parent.FullName;
var backupRoot = Path.Combine(testRootParent, "backup");
var backupRoot = Path.Combine(testRoot, "backup");
var migratableArtifacts = GetProjectJsonArtifacts(testRoot);
@ -47,9 +45,7 @@ namespace Microsoft.DotNet.Migration.Tests
.CreateTestInstance(testProjectName)
.Path;
var testRootParent = new DirectoryInfo(testRoot).Parent.FullName;
var backupRoot = Path.Combine(testRootParent, "backup", testProjectName);
var backupRoot = Path.Combine(testRoot, "backup");
var migratableArtifacts = GetProjectJsonArtifacts(testRoot);

View file

@ -3,6 +3,7 @@
using Microsoft.Build.Construction;
using Microsoft.DotNet.TestFramework;
using Microsoft.DotNet.Tools.Common;
using Microsoft.DotNet.Tools.Test.Utilities;
using System;
using System.Collections.Generic;
@ -573,7 +574,10 @@ namespace Microsoft.DotNet.Migration.Tests
private void VerifyMigration(IEnumerable<string> expectedProjects, string rootDir)
{
var backupDir = Path.Combine(rootDir, "backup");
var migratedProjects = Directory.EnumerateFiles(rootDir, "*.csproj", SearchOption.AllDirectories)
.Where(s => !PathUtility.IsChildOfDirectory(backupDir, s))
.Where(s => Directory.EnumerateFiles(Path.GetDirectoryName(s), "*.csproj").Count() == 1)
.Where(s => Path.GetFileName(Path.GetDirectoryName(s)).Contains("Project"))
.Select(s => Path.GetFileName(Path.GetDirectoryName(s)));