Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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