From ef215e81e827d89e4cc511f9588eb8fdd9f40175 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 31 Jan 2025 10:08:46 +0100 Subject: [PATCH] Implement whole folder moves to the `mv` command (#385) * Implement whole folder moves to the `mv` command * dotnet format * Ensure test does not depend on order of returning files, this may differ machine to machine * dotnet format --- src/docs-mover/Move.cs | 205 ++++++++++++------ .../Mover/MoverTests.cs | 79 +++++-- 2 files changed, 201 insertions(+), 83 deletions(-) diff --git a/src/docs-mover/Move.cs b/src/docs-mover/Move.cs index 9ff7916c..f6cee632 100644 --- a/src/docs-mover/Move.cs +++ b/src/docs-mover/Move.cs @@ -11,30 +11,43 @@ namespace Documentation.Mover; +public record ChangeSet(IFileInfo From, IFileInfo To); +public record Change(IFileInfo Source, string OriginalContent, string NewContent); +public record LinkModification(string OldLink, string NewLink, string SourceFile, int LineNumber, int ColumnNumber); + public class Move(IFileSystem readFileSystem, IFileSystem writeFileSystem, DocumentationSet documentationSet, ILoggerFactory loggerFactory) { - private readonly ILogger _logger = loggerFactory.CreateLogger(); - private readonly List<(string filePath, string originalContent, string newContent)> _changes = []; - private readonly List _linkModifications = []; private const string ChangeFormatString = "Change \e[31m{0}\e[0m to \e[32m{1}\e[0m at \e[34m{2}:{3}:{4}\e[0m"; - public record LinkModification(string OldLink, string NewLink, string SourceFile, int LineNumber, int ColumnNumber); - + private readonly ILogger _logger = loggerFactory.CreateLogger(); + private readonly Dictionary> _changes = []; + private readonly Dictionary> _linkModifications = []; - public ReadOnlyCollection LinkModifications => _linkModifications.AsReadOnly(); + public IReadOnlyDictionary> LinkModifications => _linkModifications.AsReadOnly(); + public IReadOnlyCollection Changes => _changes.Keys; public async Task Execute(string source, string target, bool isDryRun, Cancel ctx = default) { if (isDryRun) _logger.LogInformation("Running in dry-run mode"); - if (!ValidateInputs(source, target, out var from, out var to)) + if (!ValidateInputs(source, target, out var fromFiles, out var toFiles)) return 1; - var sourcePath = from.FullName; - var targetPath = to.FullName; + foreach (var (fromFile, toFile) in fromFiles.Zip(toFiles)) + { + var changeSet = new ChangeSet(fromFile, toFile); + _logger.LogInformation($"Requested to move from '{fromFile}' to '{toFile}"); + await SetupChanges(changeSet, ctx); + } + + return await MoveAndRewriteLinks(isDryRun, ctx); + } - _logger.LogInformation($"Requested to move from '{from}' to '{to}"); + private async Task SetupChanges(ChangeSet changeSet, Cancel ctx) + { + var sourcePath = changeSet.From.FullName; + var targetPath = changeSet.To.FullName; var sourceContent = await readFileSystem.File.ReadAllTextAsync(sourcePath, ctx); @@ -61,7 +74,10 @@ public async Task Execute(string source, string target, bool isDryRun, Canc var newLink = $"[{match.Groups[1].Value}]({newPath})"; var lineNumber = sourceContent.Substring(0, match.Index).Count(c => c == '\n') + 1; var columnNumber = match.Index - sourceContent.LastIndexOf('\n', match.Index); - _linkModifications.Add(new LinkModification( + if (!_linkModifications.ContainsKey(changeSet)) + _linkModifications[changeSet] = []; + + _linkModifications[changeSet].Add(new LinkModification( match.Value, newLink, sourcePath, @@ -71,94 +87,155 @@ public async Task Execute(string source, string target, bool isDryRun, Canc return newLink; }); - _changes.Add((sourcePath, sourceContent, change)); + _changes[changeSet] = [new Change(changeSet.From, sourceContent, change)]; foreach (var (_, markdownFile) in documentationSet.MarkdownFiles) { await ProcessMarkdownFile( - sourcePath, - targetPath, + changeSet, markdownFile, ctx ); } - foreach (var (oldLink, newLink, sourceFile, lineNumber, columnNumber) in LinkModifications) + } + + private async Task MoveAndRewriteLinks(bool isDryRun, Cancel ctx) + { + foreach (var (changeSet, linkModifications) in _linkModifications) { - _logger.LogInformation(string.Format( - ChangeFormatString, - oldLink, - newLink, - sourceFile == sourcePath && !isDryRun ? targetPath : sourceFile, - lineNumber, - columnNumber - )); + foreach (var (oldLink, newLink, sourceFile, lineNumber, columnNumber) in linkModifications) + { + _logger.LogInformation(string.Format( + ChangeFormatString, + oldLink, + newLink, + sourceFile == changeSet.From.FullName && !isDryRun ? changeSet.To.FullName : sourceFile, + lineNumber, + columnNumber + )); + } } if (isDryRun) return 0; - try { - foreach (var (filePath, _, newContent) in _changes) - await writeFileSystem.File.WriteAllTextAsync(filePath, newContent, ctx); - var targetDirectory = Path.GetDirectoryName(targetPath); - readFileSystem.Directory.CreateDirectory(targetDirectory!); - readFileSystem.File.Move(sourcePath, targetPath); + foreach (var (changeSet, changes) in _changes) + { + foreach (var (filePath, _, newContent) in changes) + { + if (!filePath.Directory!.Exists) + writeFileSystem.Directory.CreateDirectory(filePath.Directory.FullName); + await writeFileSystem.File.WriteAllTextAsync(filePath.FullName, newContent, ctx); + + } + + var targetDirectory = Path.GetDirectoryName(changeSet.To.FullName); + readFileSystem.Directory.CreateDirectory(targetDirectory!); + readFileSystem.File.Move(changeSet.From.FullName, changeSet.To.FullName); + } } catch (Exception) { - foreach (var (filePath, originalContent, _) in _changes) - await writeFileSystem.File.WriteAllTextAsync(filePath, originalContent, ctx); - writeFileSystem.File.Move(targetPath, sourcePath); - _logger.LogError("An error occurred while moving files. Reverting changes"); + if (_changes.Count > 1) + { + _logger.LogError("An error occurred while moving files. Can only revert a single file move at this time"); + throw; + } + + foreach (var (changeSet, changes) in _changes) + { + foreach (var (filePath, originalContent, _) in changes) + await writeFileSystem.File.WriteAllTextAsync(filePath.FullName, originalContent, ctx); + if (!changeSet.To.Exists) + writeFileSystem.File.Move(changeSet.To.FullName, changeSet.From.FullName); + else + writeFileSystem.File.Copy(changeSet.To.FullName, changeSet.From.FullName, overwrite: true); + _logger.LogError("An error occurred while moving files. Reverting changes"); + } throw; } + return 0; } - private bool ValidateInputs(string source, string target, out IFileInfo from, out IFileInfo to) + private bool ValidateInputs(string source, string target, out IFileInfo[] fromFiles, out IFileInfo[] toFiles) { - from = readFileSystem.FileInfo.New(source); - to = readFileSystem.FileInfo.New(target); + fromFiles = []; + toFiles = []; + + var fromFile = readFileSystem.FileInfo.New(source); + var fromDirectory = readFileSystem.DirectoryInfo.New(source); + var toFile = readFileSystem.FileInfo.New(target); + var toDirectory = readFileSystem.DirectoryInfo.New(target); - if (!from.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase)) + //from does not exist at all + if (!fromFile.Exists && !fromDirectory.Exists) { - _logger.LogError("Source path must be a markdown file. Directory paths are not supported yet"); + _logger.LogError(!string.IsNullOrEmpty(fromFile.Extension) + ? $"Source file '{fromFile}' does not exist" + : $"Source directory '{fromDirectory}' does not exist"); return false; } + //moving file + if (fromFile.Exists) + { + if (!fromFile.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogError("Source path must be a markdown file. Directory paths are not supported yet"); + return false; + } - if (to.Extension == string.Empty) - to = readFileSystem.FileInfo.New(Path.Combine(to.FullName, from.Name)); + //if toFile has no extension assume move to folder + if (toFile.Extension == string.Empty) + toFile = readFileSystem.FileInfo.New(Path.Combine(toDirectory.FullName, fromFile.Name)); - if (!to.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogError($"Target path '{to.FullName}' must be a markdown file."); - return false; + if (!toFile.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogError($"Target path '{toFile.FullName}' must be a markdown file."); + return false; + } + if (toFile.Exists) + { + _logger.LogError($"Target file {target} already exists"); + return false; + } + fromFiles = [fromFile]; + toFiles = [toFile]; } - - if (!from.Exists) + //moving folder + else if (fromDirectory.Exists) { - _logger.LogError($"Source file {source} does not exist"); - return false; - } + if (toDirectory.Exists) + { + _logger.LogError($"Target directory '{toDirectory.FullName}' already exists."); + return false; + } - if (to.Exists) - { - _logger.LogError($"Target file {target} already exists"); - return false; + if (toDirectory.FullName.StartsWith(fromDirectory.FullName)) + { + _logger.LogError($"Can not move source directory '{toDirectory.FullName}' to a {toFile.FullName}"); + return false; + } + + fromFiles = fromDirectory.GetFiles("*.md", SearchOption.AllDirectories); + toFiles = fromFiles.Select(f => + { + var relative = Path.GetRelativePath(fromDirectory.FullName, f.FullName); + return readFileSystem.FileInfo.New(Path.Combine(toDirectory.FullName, relative)); + }).ToArray(); } return true; } - private async Task ProcessMarkdownFile( - string source, - string target, - MarkdownFile value, - Cancel ctx) + private async Task ProcessMarkdownFile(ChangeSet changeSet, MarkdownFile value, Cancel ctx) { + var source = changeSet.From.FullName; + var target = changeSet.To.FullName; + var content = await readFileSystem.File.ReadAllTextAsync(value.FilePath, ctx); var currentDir = Path.GetDirectoryName(value.FilePath)!; var pathInfo = GetPathInfo(currentDir, source, target); @@ -166,8 +243,8 @@ private async Task ProcessMarkdownFile( if (Regex.IsMatch(content, linkPattern)) { - var newContent = ReplaceLinks(content, linkPattern, pathInfo.absoluteStyleTarget, target, value); - _changes.Add((value.FilePath, content, newContent)); + var newContent = ReplaceLinks(changeSet, content, linkPattern, pathInfo.absoluteStyleTarget, target, value); + _changes[changeSet].Add(new Change(value.SourceFile, content, newContent)); } } @@ -196,12 +273,12 @@ private static string BuildLinkPattern( $@"\[([^\]]*)\]\((?:{pathInfo.relativeSource}|{pathInfo.relativeSourceWithDotSlash}|{pathInfo.absolutStyleSource})(?:#[^\)]*?)?\)"; private string ReplaceLinks( + ChangeSet changeSet, string content, string linkPattern, string absoluteStyleTarget, string target, - MarkdownFile value - ) => + MarkdownFile value) => Regex.Replace( content, linkPattern, @@ -227,7 +304,9 @@ MarkdownFile value var lineNumber = content.Substring(0, match.Index).Count(c => c == '\n') + 1; var columnNumber = match.Index - content.LastIndexOf('\n', match.Index); - _linkModifications.Add(new LinkModification( + if (!_linkModifications.ContainsKey(changeSet)) + _linkModifications[changeSet] = []; + _linkModifications[changeSet].Add(new LinkModification( match.Value, newLink, value.SourceFile.FullName, diff --git a/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs b/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs index e14191d5..0331719e 100644 --- a/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs +++ b/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs @@ -21,19 +21,25 @@ public async Task RelativeLinks() var mover = new Move(ReadFileSystem, WriteFileSystem, Set, LoggerFactory); await mover.Execute("testing/mover/first-page.md", "new-folder/hello-world.md", true); - mover.LinkModifications.Should().HaveCount(3); - Path.GetRelativePath(".", mover.LinkModifications[0].SourceFile).Should().Be("testing/mover/first-page.md"); - mover.LinkModifications[0].OldLink.Should().Be("[Link to second page](second-page.md)"); - mover.LinkModifications[0].NewLink.Should().Be("[Link to second page](../testing/mover/second-page.md)"); + mover.Changes.Should().HaveCount(1); + var changeSet = mover.Changes.First(); - Path.GetRelativePath(".", mover.LinkModifications[1].SourceFile).Should().Be("testing/mover/second-page.md"); - mover.LinkModifications[1].OldLink.Should().Be("[Link to first page](first-page.md)"); - mover.LinkModifications[1].NewLink.Should().Be("[Link to first page](../../new-folder/hello-world.md)"); + var linkModifications = mover.LinkModifications[changeSet]; + linkModifications.Should().HaveCount(3); - Path.GetRelativePath(".", mover.LinkModifications[2].SourceFile).Should().Be("testing/mover/second-page.md"); - mover.LinkModifications[2].OldLink.Should().Be("[Absolut link to first page](/testing/mover/first-page.md)"); - mover.LinkModifications[2].NewLink.Should().Be("[Absolut link to first page](/new-folder/hello-world.md)"); + + Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be("testing/mover/first-page.md"); + linkModifications[0].OldLink.Should().Be("[Link to second page](second-page.md)"); + linkModifications[0].NewLink.Should().Be("[Link to second page](../testing/mover/second-page.md)"); + + Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be("testing/mover/second-page.md"); + linkModifications[1].OldLink.Should().Be("[Link to first page](first-page.md)"); + linkModifications[1].NewLink.Should().Be("[Link to first page](../../new-folder/hello-world.md)"); + + Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be("testing/mover/second-page.md"); + linkModifications[2].OldLink.Should().Be("[Absolut link to first page](/testing/mover/first-page.md)"); + linkModifications[2].NewLink.Should().Be("[Absolut link to first page](/new-folder/hello-world.md)"); } [Fact] @@ -44,18 +50,51 @@ public async Task MoveToFolder() var mover = new Move(ReadFileSystem, WriteFileSystem, Set, LoggerFactory); await mover.Execute("testing/mover/first-page.md", "new-folder", true); - mover.LinkModifications.Should().HaveCount(3); - Path.GetRelativePath(".", mover.LinkModifications[0].SourceFile).Should().Be("testing/mover/first-page.md"); - mover.LinkModifications[0].OldLink.Should().Be("[Link to second page](second-page.md)"); - mover.LinkModifications[0].NewLink.Should().Be("[Link to second page](../testing/mover/second-page.md)"); + mover.Changes.Should().HaveCount(1); + var changeSet = mover.Changes.First(); + + var linkModifications = mover.LinkModifications[changeSet]; + linkModifications.Should().HaveCount(3); + + Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be("testing/mover/first-page.md"); + linkModifications[0].OldLink.Should().Be("[Link to second page](second-page.md)"); + linkModifications[0].NewLink.Should().Be("[Link to second page](../testing/mover/second-page.md)"); + + Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be("testing/mover/second-page.md"); + linkModifications[1].OldLink.Should().Be("[Link to first page](first-page.md)"); + linkModifications[1].NewLink.Should().Be("[Link to first page](../../new-folder/first-page.md)"); + + Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be("testing/mover/second-page.md"); + linkModifications[2].OldLink.Should().Be("[Absolut link to first page](/testing/mover/first-page.md)"); + linkModifications[2].NewLink.Should().Be("[Absolut link to first page](/new-folder/first-page.md)"); + } + + [Fact] + public async Task MoveFolderToFolder() + { + var workingDirectory = Set.Configuration.SourceFile.DirectoryName; + Directory.SetCurrentDirectory(workingDirectory!); + + var mover = new Move(ReadFileSystem, WriteFileSystem, Set, LoggerFactory); + await mover.Execute("testing/mover", "new-folder", true); + + mover.Changes.Should().HaveCount(2); + var changeSet = mover.LinkModifications.FirstOrDefault(k => k.Key.From.Name == "first-page.md").Key; + + var linkModifications = mover.LinkModifications[changeSet]; + linkModifications.Should().HaveCount(3); + + Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be("testing/mover/first-page.md"); + linkModifications[0].OldLink.Should().Be("[Link to second page](second-page.md)"); + linkModifications[0].NewLink.Should().Be("[Link to second page](../testing/mover/second-page.md)"); - Path.GetRelativePath(".", mover.LinkModifications[1].SourceFile).Should().Be("testing/mover/second-page.md"); - mover.LinkModifications[1].OldLink.Should().Be("[Link to first page](first-page.md)"); - mover.LinkModifications[1].NewLink.Should().Be("[Link to first page](../../new-folder/first-page.md)"); + Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be("testing/mover/second-page.md"); + linkModifications[1].OldLink.Should().Be("[Link to first page](first-page.md)"); + linkModifications[1].NewLink.Should().Be("[Link to first page](../../new-folder/first-page.md)"); - Path.GetRelativePath(".", mover.LinkModifications[2].SourceFile).Should().Be("testing/mover/second-page.md"); - mover.LinkModifications[2].OldLink.Should().Be("[Absolut link to first page](/testing/mover/first-page.md)"); - mover.LinkModifications[2].NewLink.Should().Be("[Absolut link to first page](/new-folder/first-page.md)"); + Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be("testing/mover/second-page.md"); + linkModifications[2].OldLink.Should().Be("[Absolut link to first page](/testing/mover/first-page.md)"); + linkModifications[2].NewLink.Should().Be("[Absolut link to first page](/new-folder/first-page.md)"); } }