Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion docs/contribute/redirects.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ This example strips all anchors from the source page.
Any remaining links resolving to anchors on `7th-page.md` will fail link validation.

```yaml
redirects
redirects:
'testing/redirects/7th-page.md':
to: 'testing/redirects/5th-page.md'
anchors: '!'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,10 @@ public ConfigurationFile(IDocumentationSetContext context, VersionsConfiguration
return;
}

var sourceFile = context.ConfigurationPath;
var redirectFileName = sourceFile.Name.StartsWith('_') ? "_redirects.yml" : "redirects.yml";
var redirectFileInfo = sourceFile.FileSystem.FileInfo.New(Path.Combine(sourceFile.Directory!.FullName, redirectFileName));
var redirectFile = new RedirectFile(redirectFileInfo, _context);
var redirectFile = new RedirectFile(_context);
Redirects = redirectFile.Redirects;

var sourceFile = context.ConfigurationPath;
var reader = new YamlStreamReader(sourceFile, _context.Collector);
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ namespace Elastic.Documentation.Configuration.Builder;

public record RedirectFile
{
public Dictionary<string, LinkRedirect>? Redirects { get; set; }
private IFileInfo Source { get; init; }
private IDocumentationSetContext Context { get; init; }
public Dictionary<string, LinkRedirect>? Redirects { get; }
public IFileInfo Source { get; }
private IDocumentationSetContext Context { get; }

public RedirectFile(IFileInfo source, IDocumentationSetContext context)
public RedirectFile(IDocumentationSetContext context, IFileInfo? source = null)
{
Source = source;
var docsetConfigurationPath = context.ConfigurationPath;
var redirectFileName = docsetConfigurationPath.Name.StartsWith('_') ? "_redirects.yml" : "redirects.yml";
var redirectFileInfo = docsetConfigurationPath.FileSystem.FileInfo.New(Path.Combine(docsetConfigurationPath.Directory!.FullName, redirectFileName));
Source = source ?? redirectFileInfo;
Context = context;

if (!source.Exists)
if (!Source.Exists)
return;

var reader = new YamlStreamReader(Source, Context.Collector);
Expand Down
23 changes: 23 additions & 0 deletions src/Elastic.Documentation/Extensions/IFileInfoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ public static bool IsSubPathOf(this IFileInfo file, IDirectoryInfo parentDirecto
var parent = file.Directory;
return parent is not null && parent.IsSubPathOf(parentDirectory);
}

/// Checks if <paramref name="file"/> has parent directory <paramref name="parentName"/>
public static bool HasParent(this IFileInfo file, string parentName)
{
var parent = file.Directory;
return parent is not null && parent.HasParent(parentName);
}
}

public static class IDirectoryInfoExtensions
Expand All @@ -42,4 +49,20 @@ public static bool IsSubPathOf(this IDirectoryInfo directory, IDirectoryInfo par

return false;
}

/// Checks if <paramref name="directory"/> has parent directory <paramref name="parentName"/>
public static bool HasParent(this IDirectoryInfo directory, string parentName)
{
if (directory.Name == parentName)
return true;
var parent = directory;
do
{
if (parent.Name == parentName)
return true;
parent = parent.Parent;
} while (parent != null);

return false;
}
}
50 changes: 31 additions & 19 deletions src/tooling/docs-builder/Cli/DiffCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Builder;
using Elastic.Documentation.Configuration.Versions;
using Elastic.Documentation.Extensions;
using Elastic.Documentation.Tooling.Diagnostics.Console;
using Microsoft.Extensions.Logging;

Expand All @@ -27,57 +28,68 @@ IConfigurationContext configurationContext
/// <summary>
/// Validates redirect updates in the current branch using the redirect file against changes reported by git.
/// </summary>
/// <param name="path">The baseline path to perform the check</param>
/// <param name="path"> -p, Defaults to the`{pwd}/docs` folder</param>
/// <param name="ctx"></param>
[SuppressMessage("Usage", "CA2254:Template should be a static expression")]
[Command("validate")]
public async Task<int> ValidateRedirects([Argument] string? path = null, Cancel ctx = default)
public async Task<int> ValidateRedirects(string? path = null, Cancel ctx = default)
{
var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"));
path ??= "docs";

await using var collector = new ConsoleDiagnosticsCollector(logFactory, githubActionsService).StartAsync(ctx);

var fs = new FileSystem();
var root = fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);

var buildContext = new BuildContext(collector, fs, fs, configurationContext, ExportOptions.MetadataOnly, root.FullName, null);
var sourceFile = buildContext.ConfigurationPath;
var redirectFileName = sourceFile.Name.StartsWith('_') ? "_redirects.yml" : "redirects.yml";
var redirectFileInfo = sourceFile.FileSystem.FileInfo.New(Path.Combine(sourceFile.Directory!.FullName, redirectFileName));
if (!redirectFileInfo.Exists)
var buildContext = new BuildContext(collector, fs, fs, configurationContext, ExportOptions.MetadataOnly, path, null);
var redirectFile = new RedirectFile(buildContext);
if (!redirectFile.Source.Exists)
{
await collector.StopAsync(ctx);
return 0;
}

var redirectFileParser = new RedirectFile(redirectFileInfo, buildContext);
var redirects = redirectFileParser.Redirects;
var redirects = redirectFile.Redirects;

if (redirects is null)
{
collector.EmitError(redirectFileInfo, "It was not possible to parse the redirects file.");
collector.EmitError(redirectFile.Source, "It was not possible to parse the redirects file.");
await collector.StopAsync(ctx);
return collector.Errors;
}

IRepositoryTracker tracker = runningOnCi ? new IntegrationGitRepositoryTracker(path) : new LocalGitRepositoryTracker(collector, root, path);
var root = Paths.DetermineSourceDirectoryRoot(buildContext.DocumentationSourceDirectory);
if (root is null)
{
collector.EmitError(redirectFile.Source, $"Unable to determine the root of the source directory {buildContext.DocumentationSourceDirectory}.");
await collector.StopAsync(ctx);
return collector.Errors;
}
var relativePath = Path.GetRelativePath(root.FullName, buildContext.DocumentationSourceDirectory.FullName);
_log.LogInformation("Using relative path {RelativePath} for validating changes", relativePath);
IRepositoryTracker tracker = runningOnCi ? new IntegrationGitRepositoryTracker(relativePath) : new LocalGitRepositoryTracker(collector, root, relativePath);
var changed = tracker.GetChangedFiles();

if (changed.Any())
_log.LogInformation("Found {Count} changes to files related to documentation in the current branch.", changed.Count());
if (changed.Count != 0)
_log.LogInformation("Found {Count} changes to files related to documentation in the current branch.", changed.Count);

var missingRedirects = changed
.Where(c =>
c.ChangeType is GitChangeType.Deleted or GitChangeType.Renamed
&& !redirects.ContainsKey(c is RenamedGitChange renamed ? renamed.OldFilePath : c.FilePath)
)
.Where(c => !fs.FileInfo.New(c.FilePath).HasParent("_snippets"))
.ToArray();

foreach (var notFound in changed.DistinctBy(c => c.FilePath).Where(c => c.ChangeType is GitChangeType.Deleted or GitChangeType.Renamed
&& !redirects.ContainsKey(c is RenamedGitChange renamed ? renamed.OldFilePath : c.FilePath)))
foreach (var notFound in missingRedirects)
{
if (notFound is RenamedGitChange renamed)
{
collector.EmitError(redirectFileInfo.Name,
collector.EmitError(redirectFile.Source,
$"File '{renamed.OldFilePath}' was renamed to '{renamed.NewFilePath}' but it has no redirect configuration set.");
}
else if (notFound.ChangeType is GitChangeType.Deleted)
{
collector.EmitError(redirectFileInfo.Name,
collector.EmitError(redirectFile.Source,
$"File '{notFound.FilePath}' was deleted but it has no redirect targets. This will lead to broken links.");
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/tooling/docs-builder/Tracking/IRepositoryTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ public record RenamedGitChange(string OldFilePath, string NewFilePath, GitChange

public interface IRepositoryTracker
{
IEnumerable<GitChange> GetChangedFiles();
IReadOnlyCollection<GitChange> GetChangedFiles();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,45 @@ namespace Documentation.Builder.Tracking;

public class IntegrationGitRepositoryTracker(string lookupPath) : IRepositoryTracker
{
private string LookupPath { get; } = $"{lookupPath}/";
public IEnumerable<GitChange> GetChangedFiles()
private string LookupPath { get; } = $"{lookupPath.Trim(['/', '\\'])}/";
public IReadOnlyCollection<GitChange> GetChangedFiles()
{
var deletedFiles = Environment.GetEnvironmentVariable("DELETED_FILES") ?? string.Empty;
if (!string.IsNullOrEmpty(deletedFiles))
{
foreach (var file in deletedFiles.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(f => f.StartsWith(LookupPath)))
yield return new GitChange(file, GitChangeType.Deleted);
}
return GetChanges().ToArray();

var addedFiles = Environment.GetEnvironmentVariable("ADDED_FILES");
if (!string.IsNullOrEmpty(addedFiles))
IEnumerable<GitChange> GetChanges()
{
foreach (var file in addedFiles.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(f => f.StartsWith(LookupPath)))
yield return new GitChange(file, GitChangeType.Added);
}
var deletedFiles = Environment.GetEnvironmentVariable("DELETED_FILES") ?? string.Empty;
if (!string.IsNullOrEmpty(deletedFiles))
{
foreach (var file in deletedFiles.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(f => f.StartsWith(LookupPath)))
yield return new GitChange(file, GitChangeType.Deleted);
}

var modifiedFiles = Environment.GetEnvironmentVariable("MODIFIED_FILES");
if (!string.IsNullOrEmpty(modifiedFiles))
{
foreach (var file in modifiedFiles.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(f => f.StartsWith(LookupPath)))
yield return new GitChange(file, GitChangeType.Modified);
}
var addedFiles = Environment.GetEnvironmentVariable("ADDED_FILES");
if (!string.IsNullOrEmpty(addedFiles))
{
foreach (var file in addedFiles.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(f => f.StartsWith(LookupPath)))
yield return new GitChange(file, GitChangeType.Added);
}

var renamedFiles = Environment.GetEnvironmentVariable("RENAMED_FILES");
if (!string.IsNullOrEmpty(renamedFiles))
{
foreach (var pair in renamedFiles.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(f => f.StartsWith(LookupPath)))
var modifiedFiles = Environment.GetEnvironmentVariable("MODIFIED_FILES");
if (!string.IsNullOrEmpty(modifiedFiles))
{
var parts = pair.Split(':');
if (parts.Length == 2)
yield return new RenamedGitChange(parts[0], parts[1], GitChangeType.Renamed);
foreach (var file in modifiedFiles.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(f => f.StartsWith(LookupPath)))
yield return new GitChange(file, GitChangeType.Modified);
}

var renamedFiles = Environment.GetEnvironmentVariable("RENAMED_FILES");
if (!string.IsNullOrEmpty(renamedFiles))
{
foreach (var pair in renamedFiles.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(f => f.StartsWith(LookupPath)))
{
var parts = pair.Split(':');
if (parts.Length == 2)
yield return new RenamedGitChange(parts[0], parts[1], GitChangeType.Renamed);
}
}

}
}
}
14 changes: 9 additions & 5 deletions src/tooling/docs-builder/Tracking/LocalGitRepositoryTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ namespace Documentation.Builder.Tracking;

public class LocalGitRepositoryTracker(DiagnosticsCollector collector, IDirectoryInfo workingDirectory, string lookupPath) : ExternalCommandExecutor(collector, workingDirectory), IRepositoryTracker
{
private string LookupPath { get; } = lookupPath;
private string LookupPath { get; } = lookupPath.Trim('\\', '/');

public IEnumerable<GitChange> GetChangedFiles()
public IReadOnlyCollection<GitChange> GetChangedFiles()
{
var defaultBranch = GetDefaultBranch();
var commitChanges = CaptureMultiple("git", "diff", "--name-status", $"{defaultBranch}...HEAD", "--", $"./{LookupPath}");
var localChanges = CaptureMultiple("git", "status", "--porcelain");
ExecInSilent([], "git", "stash", "push", "--", $"./{LookupPath}");
var localUnstagedChanges = CaptureMultiple("git", "stash", "show", "--name-status", "-u");
ExecInSilent([], "git", "stash", "pop");
var gitStashDocsFolder = Capture("git", "stash", "push", "--", $"./{LookupPath}");
var localUnstagedChanges = Array.Empty<string>();
if (gitStashDocsFolder != "No local changes to save")
{
localUnstagedChanges = CaptureMultiple("git", "stash", "show", "--name-status", "-u");
ExecInSilent([], "git", "stash", "pop");
}

return [.. GetCommitChanges(commitChanges), .. GetLocalChanges(localChanges), .. GetCommitChanges(localUnstagedChanges)];
}
Expand Down
2 changes: 1 addition & 1 deletion tests/authoring/Framework/CrossLinkResolverAssertions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module CrossLinkResolverAssertions =
member _.ConfigurationPath = mockFileSystem.FileInfo.New("mock_docset.yml")
member _.OutputDirectory = mockFileSystem.DirectoryInfo.New(".artifacts")
}
let redirectFileParser = RedirectFile(mockRedirectsFile, docContext)
let redirectFileParser = RedirectFile(docContext, mockRedirectsFile)
redirectFileParser.Redirects

let private createFetchedCrossLinks (redirectsYamlSnippet: string) (linksData: IDictionary<string, LinkMetadata>) repoName =
Expand Down
Loading