diff --git a/Directory.Packages.props b/Directory.Packages.props index 630374ac7ab..8423655a5bb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,19 +3,17 @@ true - + - - + - - + @@ -36,18 +34,17 @@ - - - + + - + \ No newline at end of file diff --git a/src/Docfx.App/RunBuild.cs b/src/Docfx.App/RunBuild.cs index 94aff36f1e1..99f49b6f2d3 100644 --- a/src/Docfx.App/RunBuild.cs +++ b/src/Docfx.App/RunBuild.cs @@ -30,12 +30,6 @@ public static string Exec(BuildJsonConfig config, BuildOptions options, string c EnvironmentContext.SetGitFeaturesDisabled(config.DisableGitFeatures); EnvironmentContext.SetBaseDirectory(Path.GetFullPath(string.IsNullOrEmpty(configDirectory) ? Directory.GetCurrentDirectory() : configDirectory)); - if (!config.DisableGitFeatures) - { - // Initialize Lazy property by ThreadPool thread.(It takes about 50-100 ms) - Task.Run(() => GitUtility.ExistGitCommand.Value); - } - // TODO: remove BaseDirectory from Config, it may cause potential issue when abused var baseDirectory = EnvironmentContext.BaseDirectory; var outputFolder = Path.GetFullPath(Path.Combine( diff --git a/src/Docfx.Build.SchemaDriven/OverwriteApplier.cs b/src/Docfx.Build.SchemaDriven/OverwriteApplier.cs index bb19be7e570..6284264e381 100644 --- a/src/Docfx.Build.SchemaDriven/OverwriteApplier.cs +++ b/src/Docfx.Build.SchemaDriven/OverwriteApplier.cs @@ -105,9 +105,9 @@ public object BuildOverwriteWithSchema(FileModel owModel, OverwriteDocumentModel { ["remote"] = overwrite.Documentation.Remote == null ? null : new Dictionary { - ["path"] = overwrite.Documentation.Remote.RelativePath, - ["branch"] = overwrite.Documentation.Remote.RemoteBranch, - ["repo"] = overwrite.Documentation.Remote.RemoteRepositoryUrl, + ["path"] = overwrite.Documentation.Remote.Path, + ["branch"] = overwrite.Documentation.Remote.Branch, + ["repo"] = overwrite.Documentation.Remote.Repo, } ["path"] = overwrite.Documentation?.Path, ["startLine"] = overwrite.Documentation?.StartLine ?? 0, diff --git a/src/Docfx.Common/Docfx.Common.csproj b/src/Docfx.Common/Docfx.Common.csproj index af670cf14a9..e2a10433b97 100644 --- a/src/Docfx.Common/Docfx.Common.csproj +++ b/src/Docfx.Common/Docfx.Common.csproj @@ -3,4 +3,7 @@ + + + diff --git a/src/Docfx.Common/Git/GitDetail.cs b/src/Docfx.Common/Git/GitDetail.cs index 69cdf529f3c..181efbf6748 100644 --- a/src/Docfx.Common/Git/GitDetail.cs +++ b/src/Docfx.Common/Git/GitDetail.cs @@ -6,49 +6,20 @@ namespace Docfx.Common.Git; -public class GitDetail +public record GitDetail { /// /// Relative path of current file to the Git Root Directory /// [YamlMember(Alias = "path")] [JsonProperty("path")] - public string RelativePath { get; set; } + public string Path { get; set; } [YamlMember(Alias = "branch")] [JsonProperty("branch")] - public string RemoteBranch { get; set; } + public string Branch { get; set; } [YamlMember(Alias = "repo")] [JsonProperty("repo")] - public string RemoteRepositoryUrl { get; set; } - - // remove it to avoid config hash changed - //[YamlMember(Alias = "commit")] - //[JsonProperty("commit")] - //public string CommitId { get; set; } - - // remove it to avoid config hash changed - //[JsonProperty("key")] - //[YamlMember(Alias = "key")] - //public string Description { get; set; } - - public override bool Equals(object obj) - { - if (obj == null) return false; - if (ReferenceEquals(this, obj)) return true; - if (GetType() != obj.GetType()) return false; - - return Equals(ToString(), obj.ToString()); - } - - public override int GetHashCode() - { - return ToString().GetHashCode(); - } - - public override string ToString() - { - return $"branch: {RemoteBranch}, url: {RemoteRepositoryUrl}, file: {RelativePath}"; - } + public string Repo { get; set; } } diff --git a/src/Docfx.Common/Git/GitRepoInfo.cs b/src/Docfx.Common/Git/GitRepoInfo.cs index 58742737239..187be9a6c01 100644 --- a/src/Docfx.Common/Git/GitRepoInfo.cs +++ b/src/Docfx.Common/Git/GitRepoInfo.cs @@ -14,16 +14,4 @@ public class GitRepoInfo public string RepoProject { get; set; } public Uri NormalizedRepoUrl { get; set; } - - public string RepoRootPath { get; set; } - - public string LocalBranch { get; set; } - - public string RemoteBranch { get; set; } - - public string RemoteOriginUrl { get; set; } - - public string RemoteHeadCommitId { get; set; } - - public string LocalHeadCommitId { get; set; } } diff --git a/src/Docfx.Common/Git/GitUtility.cs b/src/Docfx.Common/Git/GitUtility.cs index 2af47c402cc..032da7885a8 100644 --- a/src/Docfx.Common/Git/GitUtility.cs +++ b/src/Docfx.Common/Git/GitUtility.cs @@ -2,28 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text; using System.Text.RegularExpressions; -using System.Web; - using Docfx.Plugins; +using GitReader.Primitive; +using GitReader; + +#nullable enable namespace Docfx.Common.Git; public static class GitUtility { - internal const string GitTimeoutEnvVarName = "DOCFX_GIT_TIMEOUT"; - private static readonly string CommandName = "git"; - private static readonly int GitTimeOut = GetGitTimeout(); - - private static readonly string GetRepoRootCommand = "rev-parse --show-toplevel"; - private static readonly string GetLocalBranchCommand = "rev-parse --abbrev-ref HEAD"; - private static readonly string GetLocalBranchCommitIdCommand = "rev-parse HEAD"; - private static readonly string GetRemoteBranchCommand = "rev-parse --abbrev-ref @{u}"; + record Repo(string path, string url, string branch); - private static readonly Regex GitHubRepoUrlRegex = + private static Regex GitHubRepoUrlRegex = new(@"^((https|http):\/\/(.+@)?github\.com\/|git@github\.com:)(?\S+)\/(?[A-Za-z0-9_.-]+)(\.git)?\/?$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.RightToLeft); private static readonly Regex VsoGitRepoUrlRegex = @@ -32,73 +24,98 @@ public static class GitUtility private static readonly string GitHubNormalizedRepoUrlTemplate = "https://github.com/{0}/{1}"; private static readonly string VsoNormalizedRepoUrlTemplate = "https://{0}.visualstudio.com/DefaultCollection/{1}/_git/{2}"; - // TODO: only get default remote's url currently. - private static readonly string GetOriginUrlCommand = "config --get remote.origin.url"; + private static readonly ConcurrentDictionary s_cache = new(); - private static readonly string[] BuildSystemBranchName = new[] - { - "GITHUB_REF_NAME", // GitHub Actions - "APPVEYOR_REPO_BRANCH", // AppVeyor - "Git_Branch", // Team City - "CI_BUILD_REF_NAME", // GitLab CI - "GIT_LOCAL_BRANCH", // Jenkins - "GIT_BRANCH", // Jenkins - "BUILD_SOURCEBRANCHNAME" // VSO Agent - }; + private static readonly string? s_branch = + Env("DOCFX_SOURCE_BRANCH_NAME") ?? + Env("GITHUB_REF_NAME") ?? // GitHub Actions + Env("APPVEYOR_REPO_BRANCH") ?? // AppVeyor + Env("Git_Branch") ?? // Team City + Env("CI_BUILD_REF_NAME") ?? // GitLab CI + Env("GIT_LOCAL_BRANCH") ??// Jenkins + Env("GIT_BRANCH") ?? // Jenkins + Env("BUILD_SOURCEBRANCHNAME"); // VSO Agent - private static readonly ConcurrentDictionary Cache = new(); + private static string? Env(string name) => Environment.GetEnvironmentVariable(name) is { } value && !string.IsNullOrEmpty(value) ? value : null; - /// - /// Gets git is installed globally or not. - /// - public static Lazy ExistGitCommand = new(() => - { - try - { - bool gitCommandExists = CommandUtility.ExistCommand(CommandName); - if (!gitCommandExists) - { - Logger.LogInfo("Looks like Git is not installed globally. We depend on Git to extract repository information for source code and files."); - } - return gitCommandExists; - } - catch(Exception ex) - { - Logger.LogWarning("Failed to get Git command that installed globally. Exception: " + ex.ToString()); - return false; - } - }, LazyThreadSafetyMode.ExecutionAndPublication); - - public static GitDetail TryGetFileDetail(string filePath) + public static GitDetail? TryGetFileDetail(string filePath) { if (EnvironmentContext.GitFeaturesDisabled) return null; - try - { - return GetFileDetail(filePath); - } - catch (Exception ex) - { - Logger.LogWarning($"Skipping GetFileDetail. Exception found: {ex.GetType()}, Message: {ex.Message}"); - Logger.LogVerbose(ex.ToString()); - } + var repo = GetRepoInfo(Path.GetDirectoryName(filePath)); + if (repo is null) + return null; - return null; + return new() + { + Repo = repo.url, + Branch = repo.branch, + Path = Path.GetRelativePath(repo.path, filePath), + }; } - public static string RawContentUrlToContentUrl(string rawUrl) + public static string? RawContentUrlToContentUrl(string rawUrl) { if (EnvironmentContext.GitFeaturesDisabled) return null; - var branch = Environment.GetEnvironmentVariable("DOCFX_SOURCE_BRANCH_NAME"); - // GitHub return Regex.Replace( rawUrl, @"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", - string.IsNullOrEmpty(branch) ? "https://github.com/$1/$2/blob/$3/$4" : $"https://github.com/$1/$2/blob/{branch}/$4"); + string.IsNullOrEmpty(s_branch) ? "https://github.com/$1/$2/blob/$3/$4" : $"https://github.com/$1/$2/blob/{s_branch}/$4"); + } + + private static Repo? GetRepoInfo(string? directory) + { + if (string.IsNullOrEmpty(directory)) + return null; + + return s_cache.GetOrAdd(directory, _ => + { + if (IsGitRoot(directory)) + { + return GetRepoInfoCore(directory).Result; + } + + return GetRepoInfo(Path.GetDirectoryName(directory)); + }); + + static async Task GetRepoInfoCore(string directory) + { + using var repo = await Repository.Factory.OpenPrimitiveAsync(directory); + + var url = repo.RemoteUrls.FirstOrDefault(r => r.Key == "origin").Value + ?? repo.RemoteUrls.FirstOrDefault().Value; + + if (string.IsNullOrEmpty(url)) + return null; + + var branch = s_branch ?? await GetBranchName(); + if (string.IsNullOrEmpty(branch)) + return null; + + return new(directory, url, branch); + + async Task GetBranchName() + { + if (await repo.GetCurrentHeadReferenceAsync() is { } head) + { + return head.Name == "HEAD" ? head.Target.ToString() : head.Name; + } + return null; + } + } + + static bool IsGitRoot(string directory) + { + var gitPath = Path.Combine(directory, ".git"); + + // GitReader does not support .git as file in submodules + // https://github.com/kekyo/GitReader/blob/4126738704ee8b6e330e99c6bc81e9de8bc925c9/GitReader.Core/Primitive/PrimitiveRepositoryFacade.cs#L27-L30 + return Directory.Exists(gitPath); + } } [Obsolete("Docfx parses repoUrl in template preprocessor. This method is never used.")] @@ -150,241 +167,4 @@ public static GitRepoInfo Parse(string repoUrl) throw new NotSupportedException($"'{repoUrl}' is not a valid Vso/GitHub repository url"); } - - public static Uri CombineUrl(string normalizedRepoUrl, string refName, string relativePathToRepoRoot, RepoType repoType) - { - switch (repoType) - { - case RepoType.GitHub: - return new Uri(Path.Combine(normalizedRepoUrl, "blob", refName, relativePathToRepoRoot)); - case RepoType.Vso: - var rootedPathToRepo = "/" + relativePathToRepoRoot.ToNormalizedPath(); - return new Uri($"{normalizedRepoUrl}?path={HttpUtility.UrlEncode(rootedPathToRepo)}&version=GB{HttpUtility.UrlEncode(refName)}&_a=contents"); - default: - throw new NotSupportedException($"RepoType '{repoType}' is not supported."); - } - } - - internal static int GetGitTimeout() - { - var gitTimeoutEnvConfig = Environment.GetEnvironmentVariable(GitTimeoutEnvVarName); - if (int.TryParse(gitTimeoutEnvConfig, out var gitTimeout) && gitTimeout > 0) - { - return gitTimeout; - } - return 10_000; - } - - #region Private Methods - - private static GitDetail GetFileDetail(string filePath) - { - if (string.IsNullOrEmpty(filePath) || !ExistGitCommand.Value) - { - return null; - } - - var path = Path.Combine(EnvironmentContext.BaseDirectory, filePath).ToNormalizedPath(); - - var detail = GetFileDetailCore(path); - return detail; - } - - private static GitRepoInfo GetRepoInfo(string directory) - { - if (directory == null) - { - return null; - } - - if (IsGitRoot(directory)) - { - return Cache.GetOrAdd(directory, GetRepoInfoCore); - } - - var parentDirInfo = Directory.GetParent(directory); - if (parentDirInfo == null) - { - return null; - } - - return Cache.GetOrAdd(directory, d => GetRepoInfo(parentDirInfo.FullName)); - } - - private static bool IsGitRoot(string directory) - { - var gitPath = Path.Combine(directory, ".git"); - - // git submodule contains only a .git file instead of a .git folder - return Directory.Exists(gitPath) || File.Exists(gitPath); - } - - private static GitDetail GetFileDetailCore(string filePath) - { - string directory; - if (PathUtility.IsDirectory(filePath)) - { - directory = filePath; - } - else - { - directory = Path.GetDirectoryName(filePath); - } - - var repoInfo = Cache.GetOrAdd(directory, GetRepoInfo); - - return new GitDetail - { - // TODO: remove commit id to avoid config hash changed - // CommitId = repoInfo?.RemoteHeadCommitId, - RemoteBranch = repoInfo?.RemoteBranch, - RemoteRepositoryUrl = repoInfo?.RemoteOriginUrl, - RelativePath = PathUtility.MakeRelativePath(repoInfo?.RepoRootPath, filePath) - }; - } - - private static GitRepoInfo GetRepoInfoCore(string directory) - { - var repoRootPath = RunGitCommandAndGetLastLine(directory, GetRepoRootCommand); - - // the path of repo root got from git config file should be the same with path got from git command - Debug.Assert(FilePathComparer.OSPlatformSensitiveComparer.Equals(repoRootPath, directory)); - - var branchNames = GetBranchNames(repoRootPath); - - var originUrl = RunGitCommandAndGetLastLine(repoRootPath, GetOriginUrlCommand); - var repoInfo = new GitRepoInfo - { - // TODO: remove commit id to avoid config hash changed - //LocalHeadCommitId = RunGitCommandAndGetFirstLine(repoRootPath, GetLocalHeadIdCommand), - //RemoteHeadCommitId = TryRunGitCommandAndGetFirstLine(repoRootPath, GetRemoteHeadIdCommand), - RemoteOriginUrl = originUrl, - RepoRootPath = repoRootPath, - LocalBranch = branchNames.Item1, - RemoteBranch = branchNames.Item2 - }; - - return repoInfo; - } - - [ExcludeFromCodeCoverage(Justification = "To avoid Codecov report indirect coverage changes")] - private static Tuple GetBranchNames(string repoRootPath) - { - // Use the branch name specified by the environment variable. - var localBranch = Environment.GetEnvironmentVariable("DOCFX_SOURCE_BRANCH_NAME"); - if (!string.IsNullOrEmpty(localBranch)) - { - Logger.LogInfo($"For git repo <{repoRootPath}>, using branch '{localBranch}' from the environment variable DOCFX_SOURCE_BRANCH_NAME."); - return Tuple.Create(localBranch, localBranch); - } - - var isDetachedHead = "HEAD" == RunGitCommandAndGetLastLine(repoRootPath, GetLocalBranchCommand); - if (isDetachedHead) - { - return GetBranchNamesFromDetachedHead(repoRootPath); - } - - localBranch = RunGitCommandAndGetLastLine(repoRootPath, GetLocalBranchCommand); - string remoteBranch; - try - { - remoteBranch = RunGitCommandAndGetLastLine(repoRootPath, GetRemoteBranchCommand); - var index = remoteBranch.IndexOf('/'); - if (index > 0) - { - remoteBranch = remoteBranch.Substring(index + 1); - } - } - catch (Exception ex) - { - Logger.LogInfo($"For git repo <{repoRootPath}>, can't find remote branch in this repo and fallback to use local branch [{localBranch}]: {ex.Message}"); - remoteBranch = localBranch; - } - return Tuple.Create(localBranch, remoteBranch); - } - - // Many build systems use a "detached head", which means that the normal git commands - // to get branch names do not work. Thankfully, they set an environment variable. - [ExcludeFromCodeCoverage(Justification = "To avoid Codecov report indirect coverage changes")] - private static Tuple GetBranchNamesFromDetachedHead(string repoRootPath) - { - foreach (var name in BuildSystemBranchName) - { - var branchName = Environment.GetEnvironmentVariable(name); - if (!string.IsNullOrEmpty(branchName)) - { - Logger.LogInfo($"For git repo <{repoRootPath}>, using branch '{branchName}' from the environment variable {name}."); - return Tuple.Create(branchName, branchName); - } - } - - // Use the comment id as the branch name. - var commitId = RunGitCommandAndGetLastLine(repoRootPath, GetLocalBranchCommitIdCommand); - Logger.LogInfo($"For git repo <{repoRootPath}>, using commit id {commitId} as the branch name."); - return Tuple.Create(commitId, commitId); - } - - private static void ProcessErrorMessage(string message) - { - throw new GitException(message); - } - - private static string RunGitCommandAndGetLastLine(string repoPath, string arguments) - { - string content = null; - RunGitCommand(repoPath, arguments, output => content = output); - - if (string.IsNullOrEmpty(content)) - { - throw new GitException("The result can't be null or empty string"); - } - return content; - } - - private static void RunGitCommand(string repoPath, string arguments, Action processOutput) - { - var encoding = Encoding.UTF8; - const int bufferSize = 4096; - - if (!Directory.Exists(repoPath)) - { - throw new ArgumentException($"Can't find repo: {repoPath}"); - } - - using var outputStream = new MemoryStream(); - using var errorStream = new MemoryStream(); - int exitCode; - - using var outputStreamWriter = new StreamWriter(outputStream, encoding, bufferSize, true); - using var errorStreamWriter = new StreamWriter(errorStream, encoding, bufferSize, true); - exitCode = CommandUtility.RunCommand(new CommandInfo - { - Name = CommandName, - Arguments = arguments, - WorkingDirectory = repoPath, - }, outputStreamWriter, errorStreamWriter, GitTimeOut); - - // writer streams have to be flushed before reading from memory streams - // make sure that StreamWriter is not closed before reading from memory stream - outputStreamWriter.Flush(); - errorStreamWriter.Flush(); - - if (exitCode != 0) - { - errorStream.Position = 0; - using var errorStreamReader = new StreamReader(errorStream, encoding, false, bufferSize, true); - ProcessErrorMessage(errorStreamReader.ReadToEnd()); - } - else - { - outputStream.Position = 0; - using var streamReader = new StreamReader(outputStream, encoding, false, bufferSize, true); - string line; - while ((line = streamReader.ReadLine()) != null) - { - processOutput(line); - } - } - } - #endregion } diff --git a/src/Docfx.Dotnet/Parsers/XmlComment.cs b/src/Docfx.Dotnet/Parsers/XmlComment.cs index 38181309f8e..ffc39ab2e73 100644 --- a/src/Docfx.Dotnet/Parsers/XmlComment.cs +++ b/src/Docfx.Dotnet/Parsers/XmlComment.cs @@ -247,7 +247,7 @@ private Dictionary GetListContent(XPathNavigator navigator, stri { if (result.ContainsKey(name)) { - string path = context.Source?.Remote != null ? Path.Combine(EnvironmentContext.BaseDirectory, context.Source.Remote.RelativePath) : context.Source?.Path; + string path = context.Source?.Remote != null ? Path.Combine(EnvironmentContext.BaseDirectory, context.Source.Remote.Path) : context.Source?.Path; Logger.LogWarning($"Duplicate {contentType} '{name}' found in comments, the latter one is ignored.", file: StringExtension.ToDisplayPath(path), line: context.Source?.StartLine.ToString()); } else diff --git a/test/Docfx.Build.UniversalReference.Tests/UniversalReferenceDocumentProcessorTest.cs b/test/Docfx.Build.UniversalReference.Tests/UniversalReferenceDocumentProcessorTest.cs index 5a3a46441d5..5161142a490 100644 --- a/test/Docfx.Build.UniversalReference.Tests/UniversalReferenceDocumentProcessorTest.cs +++ b/test/Docfx.Build.UniversalReference.Tests/UniversalReferenceDocumentProcessorTest.cs @@ -93,8 +93,8 @@ public void ProcessPythonModelShouldSucceed() Assert.Equal("Value", classModel.Name[0].Value); Assert.Equal("cntk.core.Value", classModel.FullName[0].Value); - Assert.Equal("https://github.com/Microsoft/CNTK", classModel.Source[0].Value.Remote.RemoteRepositoryUrl); - Assert.Equal("cntk/core.py", classModel.Source[0].Value.Remote.RelativePath); + Assert.Equal("https://github.com/Microsoft/CNTK", classModel.Source[0].Value.Remote.Repo); + Assert.Equal("cntk/core.py", classModel.Source[0].Value.Remote.Path); Assert.Equal(182, classModel.Source[0].Value.StartLine); Assert.Equal(6, classModel.Syntax.Parameters.Count); diff --git a/test/Docfx.Common.Tests/GitUtilityTest.cs b/test/Docfx.Common.Tests/GitUtilityTest.cs index f59e44377fc..88d8907dfc5 100644 --- a/test/Docfx.Common.Tests/GitUtilityTest.cs +++ b/test/Docfx.Common.Tests/GitUtilityTest.cs @@ -26,20 +26,7 @@ public void Dispose() public void Environment_ForBranchName() { var info = GitUtility.TryGetFileDetail(Directory.GetCurrentDirectory()); - Assert.Equal("special-branch", info.RemoteBranch); - } - - [Fact] - public void Environment_ForGitTimeout() - { - Environment.SetEnvironmentVariable(GitUtility.GitTimeoutEnvVarName, "3000"); - Assert.Equal(3000, GitUtility.GetGitTimeout()); - - Environment.SetEnvironmentVariable(GitUtility.GitTimeoutEnvVarName, "0"); - Assert.Equal(10_000, GitUtility.GetGitTimeout()); - - Environment.SetEnvironmentVariable(GitUtility.GitTimeoutEnvVarName, ""); - Assert.Equal(10_000, GitUtility.GetGitTimeout()); + Assert.Equal("special-branch", info.Branch); } [Obsolete("It will be removed in a future version.")] @@ -68,17 +55,4 @@ public void TestParseGitRepoInfo() Assert.Equal("FakeProject", repoInfo.RepoProject); Assert.Equal(RepoType.Vso, repoInfo.RepoType); } - - [Obsolete("It will be removed in a future version.")] - [Fact] - public void TestCombineGitUrl() - { - var repoInfo = GitUtility.Parse("git@github.com:dotnet/docfx"); - var url = GitUtility.CombineUrl(repoInfo.NormalizedRepoUrl.AbsoluteUri, "dev", "src/docfx/Program.cs", RepoType.GitHub); - Assert.Equal("https://github.com/dotnet/docfx/blob/dev/src/docfx/Program.cs", url.AbsoluteUri); - - repoInfo = GitUtility.Parse("https://mseng.visualstudio.com/FakeProject/_git/Docfx"); - url = GitUtility.CombineUrl(repoInfo.NormalizedRepoUrl.AbsoluteUri, "dev", "src/docfx/Program.cs", RepoType.Vso); - Assert.Equal("https://mseng.visualstudio.com/DefaultCollection/FakeProject/_git/Docfx?path=%2fsrc%2fdocfx%2fProgram.cs&version=GBdev&_a=contents", url.AbsoluteUri); - } } diff --git a/test/docfx.Snapshot.Tests/SamplesTest.SeedHtml/html/articles-markdown.html-tabs-windows-2Ctypescript-markdown-extensions.verified.html b/test/docfx.Snapshot.Tests/SamplesTest.SeedHtml/html/articles-markdown.html-tabs-windows-2Ctypescript-markdown-extensions.verified.html index b31ba72f087..ab8d9a06654 100644 --- a/test/docfx.Snapshot.Tests/SamplesTest.SeedHtml/html/articles-markdown.html-tabs-windows-2Ctypescript-markdown-extensions.verified.html +++ b/test/docfx.Snapshot.Tests/SamplesTest.SeedHtml/html/articles-markdown.html-tabs-windows-2Ctypescript-markdown-extensions.verified.html @@ -362,7 +362,7 @@

Mermaid Diagrams
Text
One
Two
Hard
Round
Decision
Result 1
Result 2
+" data-processed="true">
Text
One
Two
Hard
Round
Decision
Result 1
Result 2

Code Snippet

The example highlights lines 2, line 5 to 7 and lines 9 to the end of the file.

using System;
diff --git a/test/docfx.Snapshot.Tests/SamplesTest.cs b/test/docfx.Snapshot.Tests/SamplesTest.cs
index 0a3be2889da..05c3c1a2b87 100644
--- a/test/docfx.Snapshot.Tests/SamplesTest.cs
+++ b/test/docfx.Snapshot.Tests/SamplesTest.cs
@@ -176,7 +176,10 @@ static Task CompareImage(Stream received, Stream verified, string
 
         static string NormalizeHtml(string html)
         {
-            return Regex.Replace(Regex.Replace(html, "", ""), @"mermaid-\d+", "");
+            html = Regex.Replace(html, "", "");
+            html = Regex.Replace(html, @"mermaid-\d+", "");
+            html = Regex.Replace(html, @"flowchart-\w+-\d", "");
+            return html;
         }
     }