Skip to content

Commit ef215e8

Browse files
authored
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
1 parent 75ac81a commit ef215e8

File tree

2 files changed

+201
-83
lines changed

2 files changed

+201
-83
lines changed

src/docs-mover/Move.cs

+142-63
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,43 @@
1111

1212
namespace Documentation.Mover;
1313

14+
public record ChangeSet(IFileInfo From, IFileInfo To);
15+
public record Change(IFileInfo Source, string OriginalContent, string NewContent);
16+
public record LinkModification(string OldLink, string NewLink, string SourceFile, int LineNumber, int ColumnNumber);
17+
1418
public class Move(IFileSystem readFileSystem, IFileSystem writeFileSystem, DocumentationSet documentationSet, ILoggerFactory loggerFactory)
1519
{
16-
private readonly ILogger _logger = loggerFactory.CreateLogger<Move>();
17-
private readonly List<(string filePath, string originalContent, string newContent)> _changes = [];
18-
private readonly List<LinkModification> _linkModifications = [];
1920
private const string ChangeFormatString = "Change \e[31m{0}\e[0m to \e[32m{1}\e[0m at \e[34m{2}:{3}:{4}\e[0m";
2021

21-
public record LinkModification(string OldLink, string NewLink, string SourceFile, int LineNumber, int ColumnNumber);
22-
22+
private readonly ILogger _logger = loggerFactory.CreateLogger<Move>();
23+
private readonly Dictionary<ChangeSet, List<Change>> _changes = [];
24+
private readonly Dictionary<ChangeSet, List<LinkModification>> _linkModifications = [];
2325

24-
public ReadOnlyCollection<LinkModification> LinkModifications => _linkModifications.AsReadOnly();
26+
public IReadOnlyDictionary<ChangeSet, List<LinkModification>> LinkModifications => _linkModifications.AsReadOnly();
27+
public IReadOnlyCollection<ChangeSet> Changes => _changes.Keys;
2528

2629
public async Task<int> Execute(string source, string target, bool isDryRun, Cancel ctx = default)
2730
{
2831
if (isDryRun)
2932
_logger.LogInformation("Running in dry-run mode");
3033

31-
if (!ValidateInputs(source, target, out var from, out var to))
34+
if (!ValidateInputs(source, target, out var fromFiles, out var toFiles))
3235
return 1;
3336

34-
var sourcePath = from.FullName;
35-
var targetPath = to.FullName;
37+
foreach (var (fromFile, toFile) in fromFiles.Zip(toFiles))
38+
{
39+
var changeSet = new ChangeSet(fromFile, toFile);
40+
_logger.LogInformation($"Requested to move from '{fromFile}' to '{toFile}");
41+
await SetupChanges(changeSet, ctx);
42+
}
43+
44+
return await MoveAndRewriteLinks(isDryRun, ctx);
45+
}
3646

37-
_logger.LogInformation($"Requested to move from '{from}' to '{to}");
47+
private async Task SetupChanges(ChangeSet changeSet, Cancel ctx)
48+
{
49+
var sourcePath = changeSet.From.FullName;
50+
var targetPath = changeSet.To.FullName;
3851

3952
var sourceContent = await readFileSystem.File.ReadAllTextAsync(sourcePath, ctx);
4053

@@ -61,7 +74,10 @@ public async Task<int> Execute(string source, string target, bool isDryRun, Canc
6174
var newLink = $"[{match.Groups[1].Value}]({newPath})";
6275
var lineNumber = sourceContent.Substring(0, match.Index).Count(c => c == '\n') + 1;
6376
var columnNumber = match.Index - sourceContent.LastIndexOf('\n', match.Index);
64-
_linkModifications.Add(new LinkModification(
77+
if (!_linkModifications.ContainsKey(changeSet))
78+
_linkModifications[changeSet] = [];
79+
80+
_linkModifications[changeSet].Add(new LinkModification(
6581
match.Value,
6682
newLink,
6783
sourcePath,
@@ -71,103 +87,164 @@ public async Task<int> Execute(string source, string target, bool isDryRun, Canc
7187
return newLink;
7288
});
7389

74-
_changes.Add((sourcePath, sourceContent, change));
90+
_changes[changeSet] = [new Change(changeSet.From, sourceContent, change)];
7591

7692
foreach (var (_, markdownFile) in documentationSet.MarkdownFiles)
7793
{
7894
await ProcessMarkdownFile(
79-
sourcePath,
80-
targetPath,
95+
changeSet,
8196
markdownFile,
8297
ctx
8398
);
8499
}
85100

86-
foreach (var (oldLink, newLink, sourceFile, lineNumber, columnNumber) in LinkModifications)
101+
}
102+
103+
private async Task<int> MoveAndRewriteLinks(bool isDryRun, Cancel ctx)
104+
{
105+
foreach (var (changeSet, linkModifications) in _linkModifications)
87106
{
88-
_logger.LogInformation(string.Format(
89-
ChangeFormatString,
90-
oldLink,
91-
newLink,
92-
sourceFile == sourcePath && !isDryRun ? targetPath : sourceFile,
93-
lineNumber,
94-
columnNumber
95-
));
107+
foreach (var (oldLink, newLink, sourceFile, lineNumber, columnNumber) in linkModifications)
108+
{
109+
_logger.LogInformation(string.Format(
110+
ChangeFormatString,
111+
oldLink,
112+
newLink,
113+
sourceFile == changeSet.From.FullName && !isDryRun ? changeSet.To.FullName : sourceFile,
114+
lineNumber,
115+
columnNumber
116+
));
117+
}
96118
}
97119

98120
if (isDryRun)
99121
return 0;
100122

101-
102123
try
103124
{
104-
foreach (var (filePath, _, newContent) in _changes)
105-
await writeFileSystem.File.WriteAllTextAsync(filePath, newContent, ctx);
106-
var targetDirectory = Path.GetDirectoryName(targetPath);
107-
readFileSystem.Directory.CreateDirectory(targetDirectory!);
108-
readFileSystem.File.Move(sourcePath, targetPath);
125+
foreach (var (changeSet, changes) in _changes)
126+
{
127+
foreach (var (filePath, _, newContent) in changes)
128+
{
129+
if (!filePath.Directory!.Exists)
130+
writeFileSystem.Directory.CreateDirectory(filePath.Directory.FullName);
131+
await writeFileSystem.File.WriteAllTextAsync(filePath.FullName, newContent, ctx);
132+
133+
}
134+
135+
var targetDirectory = Path.GetDirectoryName(changeSet.To.FullName);
136+
readFileSystem.Directory.CreateDirectory(targetDirectory!);
137+
readFileSystem.File.Move(changeSet.From.FullName, changeSet.To.FullName);
138+
}
109139
}
110140
catch (Exception)
111141
{
112-
foreach (var (filePath, originalContent, _) in _changes)
113-
await writeFileSystem.File.WriteAllTextAsync(filePath, originalContent, ctx);
114-
writeFileSystem.File.Move(targetPath, sourcePath);
115-
_logger.LogError("An error occurred while moving files. Reverting changes");
142+
if (_changes.Count > 1)
143+
{
144+
_logger.LogError("An error occurred while moving files. Can only revert a single file move at this time");
145+
throw;
146+
}
147+
148+
foreach (var (changeSet, changes) in _changes)
149+
{
150+
foreach (var (filePath, originalContent, _) in changes)
151+
await writeFileSystem.File.WriteAllTextAsync(filePath.FullName, originalContent, ctx);
152+
if (!changeSet.To.Exists)
153+
writeFileSystem.File.Move(changeSet.To.FullName, changeSet.From.FullName);
154+
else
155+
writeFileSystem.File.Copy(changeSet.To.FullName, changeSet.From.FullName, overwrite: true);
156+
_logger.LogError("An error occurred while moving files. Reverting changes");
157+
}
116158
throw;
117159
}
160+
118161
return 0;
119162
}
120163

121-
private bool ValidateInputs(string source, string target, out IFileInfo from, out IFileInfo to)
164+
private bool ValidateInputs(string source, string target, out IFileInfo[] fromFiles, out IFileInfo[] toFiles)
122165
{
123-
from = readFileSystem.FileInfo.New(source);
124-
to = readFileSystem.FileInfo.New(target);
166+
fromFiles = [];
167+
toFiles = [];
168+
169+
var fromFile = readFileSystem.FileInfo.New(source);
170+
var fromDirectory = readFileSystem.DirectoryInfo.New(source);
171+
var toFile = readFileSystem.FileInfo.New(target);
172+
var toDirectory = readFileSystem.DirectoryInfo.New(target);
125173

126-
if (!from.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase))
174+
//from does not exist at all
175+
if (!fromFile.Exists && !fromDirectory.Exists)
127176
{
128-
_logger.LogError("Source path must be a markdown file. Directory paths are not supported yet");
177+
_logger.LogError(!string.IsNullOrEmpty(fromFile.Extension)
178+
? $"Source file '{fromFile}' does not exist"
179+
: $"Source directory '{fromDirectory}' does not exist");
129180
return false;
130181
}
182+
//moving file
183+
if (fromFile.Exists)
184+
{
185+
if (!fromFile.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase))
186+
{
187+
_logger.LogError("Source path must be a markdown file. Directory paths are not supported yet");
188+
return false;
189+
}
131190

132-
if (to.Extension == string.Empty)
133-
to = readFileSystem.FileInfo.New(Path.Combine(to.FullName, from.Name));
191+
//if toFile has no extension assume move to folder
192+
if (toFile.Extension == string.Empty)
193+
toFile = readFileSystem.FileInfo.New(Path.Combine(toDirectory.FullName, fromFile.Name));
134194

135-
if (!to.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase))
136-
{
137-
_logger.LogError($"Target path '{to.FullName}' must be a markdown file.");
138-
return false;
195+
if (!toFile.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase))
196+
{
197+
_logger.LogError($"Target path '{toFile.FullName}' must be a markdown file.");
198+
return false;
199+
}
200+
if (toFile.Exists)
201+
{
202+
_logger.LogError($"Target file {target} already exists");
203+
return false;
204+
}
205+
fromFiles = [fromFile];
206+
toFiles = [toFile];
139207
}
140-
141-
if (!from.Exists)
208+
//moving folder
209+
else if (fromDirectory.Exists)
142210
{
143-
_logger.LogError($"Source file {source} does not exist");
144-
return false;
145-
}
211+
if (toDirectory.Exists)
212+
{
213+
_logger.LogError($"Target directory '{toDirectory.FullName}' already exists.");
214+
return false;
215+
}
146216

147-
if (to.Exists)
148-
{
149-
_logger.LogError($"Target file {target} already exists");
150-
return false;
217+
if (toDirectory.FullName.StartsWith(fromDirectory.FullName))
218+
{
219+
_logger.LogError($"Can not move source directory '{toDirectory.FullName}' to a {toFile.FullName}");
220+
return false;
221+
}
222+
223+
fromFiles = fromDirectory.GetFiles("*.md", SearchOption.AllDirectories);
224+
toFiles = fromFiles.Select(f =>
225+
{
226+
var relative = Path.GetRelativePath(fromDirectory.FullName, f.FullName);
227+
return readFileSystem.FileInfo.New(Path.Combine(toDirectory.FullName, relative));
228+
}).ToArray();
151229
}
152230

153231
return true;
154232
}
155233

156-
private async Task ProcessMarkdownFile(
157-
string source,
158-
string target,
159-
MarkdownFile value,
160-
Cancel ctx)
234+
private async Task ProcessMarkdownFile(ChangeSet changeSet, MarkdownFile value, Cancel ctx)
161235
{
236+
var source = changeSet.From.FullName;
237+
var target = changeSet.To.FullName;
238+
162239
var content = await readFileSystem.File.ReadAllTextAsync(value.FilePath, ctx);
163240
var currentDir = Path.GetDirectoryName(value.FilePath)!;
164241
var pathInfo = GetPathInfo(currentDir, source, target);
165242
var linkPattern = BuildLinkPattern(pathInfo);
166243

167244
if (Regex.IsMatch(content, linkPattern))
168245
{
169-
var newContent = ReplaceLinks(content, linkPattern, pathInfo.absoluteStyleTarget, target, value);
170-
_changes.Add((value.FilePath, content, newContent));
246+
var newContent = ReplaceLinks(changeSet, content, linkPattern, pathInfo.absoluteStyleTarget, target, value);
247+
_changes[changeSet].Add(new Change(value.SourceFile, content, newContent));
171248
}
172249
}
173250

@@ -196,12 +273,12 @@ private static string BuildLinkPattern(
196273
$@"\[([^\]]*)\]\((?:{pathInfo.relativeSource}|{pathInfo.relativeSourceWithDotSlash}|{pathInfo.absolutStyleSource})(?:#[^\)]*?)?\)";
197274

198275
private string ReplaceLinks(
276+
ChangeSet changeSet,
199277
string content,
200278
string linkPattern,
201279
string absoluteStyleTarget,
202280
string target,
203-
MarkdownFile value
204-
) =>
281+
MarkdownFile value) =>
205282
Regex.Replace(
206283
content,
207284
linkPattern,
@@ -227,7 +304,9 @@ MarkdownFile value
227304

228305
var lineNumber = content.Substring(0, match.Index).Count(c => c == '\n') + 1;
229306
var columnNumber = match.Index - content.LastIndexOf('\n', match.Index);
230-
_linkModifications.Add(new LinkModification(
307+
if (!_linkModifications.ContainsKey(changeSet))
308+
_linkModifications[changeSet] = [];
309+
_linkModifications[changeSet].Add(new LinkModification(
231310
match.Value,
232311
newLink,
233312
value.SourceFile.FullName,

0 commit comments

Comments
 (0)