Skip to content

Commit

Permalink
Implement whole folder moves to the mv command (#385)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Mpdreamz authored Jan 31, 2025
1 parent 75ac81a commit ef215e8
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 83 deletions.
205 changes: 142 additions & 63 deletions src/docs-mover/Move.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Move>();
private readonly List<(string filePath, string originalContent, string newContent)> _changes = [];
private readonly List<LinkModification> _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<Move>();
private readonly Dictionary<ChangeSet, List<Change>> _changes = [];
private readonly Dictionary<ChangeSet, List<LinkModification>> _linkModifications = [];

public ReadOnlyCollection<LinkModification> LinkModifications => _linkModifications.AsReadOnly();
public IReadOnlyDictionary<ChangeSet, List<LinkModification>> LinkModifications => _linkModifications.AsReadOnly();
public IReadOnlyCollection<ChangeSet> Changes => _changes.Keys;

public async Task<int> 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);

Expand All @@ -61,7 +74,10 @@ public async Task<int> 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,
Expand All @@ -71,103 +87,164 @@ public async Task<int> 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<int> 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);
var linkPattern = BuildLinkPattern(pathInfo);

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));
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit ef215e8

Please sign in to comment.