Skip to content

Commit 8d59ed2

Browse files
committed
Add mv command
1 parent 49a654d commit 8d59ed2

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

src/docs-builder/Cli/Commands.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to Elasticsearch B.V under one or more agreements.
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
4+
using System.Collections.Generic;
45
using System.IO.Abstractions;
56
using Actions.Core.Services;
67
using ConsoleAppFramework;
@@ -102,4 +103,33 @@ public async Task<int> GenerateDefault(
102103
Cancel ctx = default
103104
) =>
104105
await Generate(path, output, pathPrefix, force, strict, allowIndexing, ctx);
106+
107+
108+
/// <summary>
109+
/// Move a file or folder from one location to another and update all links in the documentation
110+
/// </summary>
111+
/// <param name="source">The source file or folder path to move from</param>
112+
/// <param name="target">The target file or folder path to move to</param>
113+
/// <param name="path"> -p, Defaults to the`{pwd}` folder</param>
114+
/// <param name="dryRun">Dry run the move operation</param>
115+
/// <param name="ctx"></param>
116+
[Command("mv")]
117+
public async Task<int> Move(
118+
[Argument] string? source = null,
119+
[Argument] string? target = null,
120+
bool? dryRun = null,
121+
string? path = null,
122+
Cancel ctx = default
123+
)
124+
{
125+
var fileSystem = new FileSystem();
126+
var context = new BuildContext(fileSystem, fileSystem, path, null)
127+
{
128+
Collector = new ConsoleDiagnosticsCollector(logger, null),
129+
};
130+
var set = new DocumentationSet(context);
131+
132+
var moveCommand = new MoveCommand(fileSystem, set, logger);
133+
return await moveCommand.Execute(source, target, dryRun ?? false, ctx);
134+
}
105135
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
using System.IO.Abstractions;
2+
using Elastic.Markdown;
3+
using Elastic.Markdown.IO;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace Documentation.Builder.Cli;
7+
8+
internal class MoveCommand(IFileSystem fileSystem, DocumentationSet documentationSet, ILoggerFactory loggerFactory)
9+
{
10+
private readonly ILogger _logger = loggerFactory.CreateLogger<MoveCommand>();
11+
private readonly List<(string filePath, string originalContent,string newContent)> _changes = [];
12+
13+
public async Task<int> Execute(string? source, string? target, bool isDryRun, Cancel ctx = default)
14+
{
15+
if (isDryRun)
16+
_logger.LogInformation("Running in dry-run mode");
17+
18+
if (!ValidateInputs(source, target))
19+
{
20+
return 1;
21+
}
22+
23+
var sourcePath = Path.GetFullPath(source!);
24+
var targetPath = Path.GetFullPath(target!);
25+
26+
foreach (var (_, markdownFile) in documentationSet.MarkdownFiles)
27+
{
28+
await ProcessMarkdownFile(
29+
sourcePath,
30+
targetPath,
31+
isDryRun,
32+
markdownFile,
33+
ctx
34+
);
35+
}
36+
37+
if (isDryRun)
38+
return 0;
39+
40+
var targetDirectory = Path.GetDirectoryName(targetPath);
41+
fileSystem.Directory.CreateDirectory(targetDirectory!); // CreateDirectory automatically creates all necessary parent directories
42+
fileSystem.File.Move(sourcePath, targetPath);
43+
// Write changes to disk
44+
try {
45+
foreach (var (filePath, _, newContent) in _changes)
46+
await fileSystem.File.WriteAllTextAsync(filePath, newContent, ctx);
47+
} catch (Exception) {
48+
foreach (var (filePath, originalContent, _) in _changes)
49+
await fileSystem.File.WriteAllTextAsync(filePath, originalContent, ctx);
50+
fileSystem.File.Move(targetPath, sourcePath);
51+
throw;
52+
}
53+
54+
55+
56+
57+
return 0;
58+
}
59+
60+
private bool ValidateInputs(string? source, string? target)
61+
{
62+
63+
if (string.IsNullOrEmpty(source))
64+
{
65+
_logger.LogError("Source path is required");
66+
return false;
67+
}
68+
69+
if (string.IsNullOrEmpty(target))
70+
{
71+
_logger.LogError("Target path is required");
72+
return false;
73+
}
74+
75+
if (!Path.GetExtension(source).Equals(".md", StringComparison.OrdinalIgnoreCase))
76+
{
77+
_logger.LogError("Source path must be a markdown file. Directory paths are not supported yet");
78+
return false;
79+
}
80+
81+
if (!Path.GetExtension(target).Equals(".md", StringComparison.OrdinalIgnoreCase))
82+
{
83+
_logger.LogError("Target path must be a markdown file. Directory paths are not supported yet");
84+
return false;
85+
}
86+
87+
if (!fileSystem.File.Exists(source))
88+
{
89+
_logger.LogError($"Source file {source} does not exist");
90+
return false;
91+
}
92+
93+
if (fileSystem.File.Exists(target))
94+
{
95+
_logger.LogError($"Target file {target} already exists");
96+
return false;
97+
}
98+
99+
return true;
100+
}
101+
102+
private async Task ProcessMarkdownFile(
103+
string source,
104+
string target,
105+
bool isDryRun,
106+
MarkdownFile value,
107+
Cancel ctx)
108+
{
109+
var content = await fileSystem.File.ReadAllTextAsync(value.FilePath, ctx);
110+
var currentDir = Path.GetDirectoryName(value.FilePath)!;
111+
var pathInfo = GetPathInfo(currentDir, source, target);
112+
var linkPattern = BuildLinkPattern(pathInfo);
113+
114+
if (System.Text.RegularExpressions.Regex.IsMatch(content, linkPattern))
115+
{
116+
var newContent = ReplaceLinks(content, linkPattern, pathInfo.absoluteStyleTarget, target,value);
117+
_changes.Add((value.FilePath, content, newContent));
118+
}
119+
}
120+
121+
private (string relativeSource, string relativeSourceWithDotSlash, string absolutStyleSource, string absoluteStyleTarget) GetPathInfo(
122+
string currentDir,
123+
string sourcePath,
124+
string targetPath
125+
)
126+
{
127+
var relativeSource = Path.GetRelativePath(currentDir, sourcePath).Replace('\\', '/');
128+
var relativeSourceWithDotSlash = Path.Combine(".", relativeSource).Replace('\\', '/');
129+
var relativeToDocsFolder = Path.GetRelativePath(documentationSet.SourcePath.FullName, sourcePath).Replace('\\', '/');
130+
var absolutStyleSource = $"/{relativeToDocsFolder}".Replace('\\', '/');
131+
var relativeToDocsFolderTarget = Path.GetRelativePath(documentationSet.SourcePath.FullName, targetPath);
132+
var absoluteStyleTarget = $"/{relativeToDocsFolderTarget}".Replace('\\', '/');
133+
return (
134+
relativeSource,
135+
relativeSourceWithDotSlash,
136+
absolutStyleSource,
137+
absoluteStyleTarget
138+
);
139+
}
140+
141+
private static string BuildLinkPattern(
142+
(string relativeSource, string relativeSourceWithDotSlash, string absolutStyleSource, string _) pathInfo) =>
143+
$@"\[([^\]]*)\]\((?:{pathInfo.relativeSource}|{pathInfo.relativeSourceWithDotSlash}|{pathInfo.absolutStyleSource})(?:#[^\)]*?)?\)";
144+
145+
private string ReplaceLinks(
146+
string content,
147+
string linkPattern,
148+
string absoluteStyleTarget,
149+
string target,
150+
MarkdownFile value
151+
)
152+
{
153+
return System.Text.RegularExpressions.Regex.Replace(
154+
content,
155+
linkPattern,
156+
match =>
157+
{
158+
var originalPath = match.Value.Substring(match.Value.IndexOf('(') + 1, match.Value.LastIndexOf(')') - match.Value.IndexOf('(') - 1);
159+
var anchor = originalPath.Contains('#')
160+
? originalPath[originalPath.IndexOf('#')..]
161+
: "";
162+
163+
string newLink;
164+
if (originalPath.StartsWith("/"))
165+
{
166+
// Absolute style link
167+
newLink = $"[{match.Groups[1].Value}]({absoluteStyleTarget}{anchor})";
168+
}
169+
// else if (originalPath.StartsWith("./"))
170+
// {
171+
// // Relative link with ./ prefix
172+
// var relativeTarget = Path.Combine(".", Path.GetRelativePath(Path.GetDirectoryName(value.FilePath)!, target)).Replace('\\', '/');
173+
// newLink = $"[{match.Groups[1].Value}]({relativeTarget}{anchor})";
174+
// }
175+
else
176+
{
177+
// Relative link
178+
var relativeTarget = Path.GetRelativePath(Path.GetDirectoryName(value.FilePath)!, target).Replace('\\', '/');
179+
newLink = $"[{match.Groups[1].Value}]({relativeTarget}{anchor})";
180+
}
181+
182+
var lineNumber = content.Substring(0, match.Index).Count(c => c == '\n') + 1;
183+
_logger.LogInformation($"Change \e[31m{match.Value}\e[0m to \e[32m{newLink}\e[0m at {value.SourceFile.FullName}:{lineNumber}");
184+
return newLink;
185+
});
186+
}
187+
}

0 commit comments

Comments
 (0)