diff --git a/tracer/src/Datadog.Trace/PDBs/SourceLink/AzureDevOpsSourceLinkUrlParser.cs b/tracer/src/Datadog.Trace/PDBs/SourceLink/AzureDevOpsSourceLinkUrlParser.cs
index ee460d462c34..cee590a114db 100644
--- a/tracer/src/Datadog.Trace/PDBs/SourceLink/AzureDevOpsSourceLinkUrlParser.cs
+++ b/tracer/src/Datadog.Trace/PDBs/SourceLink/AzureDevOpsSourceLinkUrlParser.cs
@@ -82,64 +82,40 @@ internal override bool TryParseSourceLinkUrl(Uri uri, [NotNullWhen(true)] out st
return false;
}
+ ///
+ /// Builds the repository URL by locating /_apis/git/repositories/ in the path.
+ /// Works for all Azure DevOps variants:
+ /// visualstudio.com: /{project}/_apis/git/repositories/{repo}/items
+ /// dev.azure.com: /{org}/{project}/_apis/git/repositories/{repo}/items
+ /// TFS on-prem: [/{vdir}][/{collection}]/{project}/_apis/git/repositories/{repo}/items
+ /// The repo URL is everything before _apis, plus /_git/{repo}.
+ ///
private static string? BuildRepositoryUrl(Uri uri)
{
- ReadOnlySpan segment0 = default;
- ReadOnlySpan segment1 = default;
- ReadOnlySpan segment4 = default;
- ReadOnlySpan segment5 = default;
- var segmentCount = 0;
+ var path = uri.AbsolutePath;
- foreach (var segment in uri.AbsolutePath.SplitIntoSpans('/'))
+ // Find /_apis/git/repositories/ in the path
+ const string marker = "/_apis/git/repositories/";
+ var markerPos = path.IndexOf(marker, StringComparison.Ordinal);
+ if (markerPos <= 0)
{
- ReadOnlySpan span = segment;
- if (span.IsEmpty)
- {
- continue;
- }
-
- switch (segmentCount)
- {
- case 0: segment0 = span; break;
- case 1: segment1 = span; break;
- case 4: segment4 = span; break;
- case 5: segment5 = span; break;
- }
-
- segmentCount++;
+ // markerPos == 0 means nothing before _apis (no project); < 0 means not found
+ return null;
}
- if (uri.Host.EndsWith("visualstudio.com", StringComparison.OrdinalIgnoreCase))
- {
- if (segmentCount < 5)
- {
- return null;
- }
-
- // Legacy format: https://{organization}.visualstudio.com
-#if NET6_0_OR_GREATER
- return $"https://{uri.Host}/{segment0}/_git/{segment4}";
-#else
- return $"https://{uri.Host}/{segment0.ToString()}/_git/{segment4.ToString()}";
-#endif
- }
+ // The prefix path (project and any virtual dir/collection) is everything before /_apis
+ var prefixPath = path.Substring(0, markerPos);
- if (uri.Host.EndsWith("dev.azure.com", StringComparison.OrdinalIgnoreCase))
+ // Extract the repo name after /_apis/git/repositories/
+ var afterMarker = path.Substring(markerPos + marker.Length);
+ var repoEndSlash = afterMarker.IndexOf('/');
+ if (repoEndSlash <= 0)
{
- if (segmentCount < 6)
- {
- return null;
- }
-
- // New format: https://dev.azure.com/{organization}
-#if NET6_0_OR_GREATER
- return $"https://{uri.Host}/{segment0}/{segment1}/_git/{segment5}";
-#else
- return $"https://{uri.Host}/{segment0.ToString()}/{segment1.ToString()}/_git/{segment5.ToString()}";
-#endif
+ return null;
}
- Log.Error("Unsupported Azure DevOps host: {Host}", uri.Host);
- return null;
+ var repo = afterMarker.Substring(0, repoEndSlash);
+
+ return $"{uri.Scheme}://{uri.Authority}{prefixPath}/_git/{repo}";
}
}
diff --git a/tracer/src/Datadog.Trace/PDBs/SourceLink/BitBucketServerSourceLinkUrlParser.cs b/tracer/src/Datadog.Trace/PDBs/SourceLink/BitBucketServerSourceLinkUrlParser.cs
new file mode 100644
index 000000000000..83110ee3b305
--- /dev/null
+++ b/tracer/src/Datadog.Trace/PDBs/SourceLink/BitBucketServerSourceLinkUrlParser.cs
@@ -0,0 +1,109 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Datadog.Trace.Util;
+
+namespace Datadog.Trace.Pdb.SourceLink;
+
+internal sealed class BitBucketServerSourceLinkUrlParser : SourceLinkUrlParser
+{
+ ///
+ /// Extract the git commit sha and repository url from a Bitbucket Server / Data Center SourceLink mapping string.
+ /// Supports two URL forms:
+ /// >= 4.7: https://{host}[/base]/projects/{project}/repos/{repo}/raw/*?at={sha}
+ /// < 4.7: https://{host}[/base]/projects/{project}/repos/{repo}/browse/*?at={sha}&raw
+ ///
+ internal override bool TryParseSourceLinkUrl(Uri uri, [NotNullWhen(true)] out string? commitSha, [NotNullWhen(true)] out string? repositoryUrl)
+ {
+ commitSha = null;
+ repositoryUrl = null;
+
+ try
+ {
+ var path = uri.AbsolutePath;
+ var query = uri.Query;
+
+ // Bitbucket Server paths are /projects/{project}/repos/{repo}/..., so /projects/ must precede /repos/
+ var projectsIdx = path.IndexOf("/projects/", StringComparison.Ordinal);
+ var reposIdx = path.IndexOf("/repos/", StringComparison.Ordinal);
+ if (projectsIdx < 0 || reposIdx <= projectsIdx)
+ {
+ return false;
+ }
+
+ var afterRepos = path.Substring(reposIdx + "/repos/".Length);
+
+ // Find the repo name (next segment after /repos/)
+ var repoEndSlash = afterRepos.IndexOf('/');
+ if (repoEndSlash <= 0)
+ {
+ return false;
+ }
+
+ var afterRepo = afterRepos.Substring(repoEndSlash);
+
+ bool isBrowseForm;
+ if (afterRepo.StartsWith("/raw/", StringComparison.Ordinal))
+ {
+ isBrowseForm = false;
+ }
+ else if (afterRepo.StartsWith("/browse/", StringComparison.Ordinal))
+ {
+ isBrowseForm = true;
+ }
+ else
+ {
+ return false;
+ }
+
+ // Walk the query string once: extract the at={sha} pair and, for browse form, also
+ // look for a standalone "raw" flag (as opposed to an unrelated substring match).
+ ReadOnlySpan shaSpan = default;
+ var hasRawFlag = false;
+ foreach (var pair in query.SplitIntoSpans('&'))
+ {
+ ReadOnlySpan pairSpan = ((ReadOnlySpan)pair).TrimStart('?');
+ var eqIndex = pairSpan.IndexOf('=');
+ var key = eqIndex < 0 ? pairSpan : pairSpan.Slice(0, eqIndex);
+
+ if (key.SequenceEqual("at".AsSpan()) && eqIndex >= 0)
+ {
+ shaSpan = pairSpan.Slice(eqIndex + 1);
+ }
+ else if (key.SequenceEqual("raw".AsSpan()))
+ {
+ hasRawFlag = true;
+ }
+ }
+
+ if (isBrowseForm && !hasRawFlag)
+ {
+ return false;
+ }
+
+ if (!IsValidCommitSha(shaSpan))
+ {
+ return false;
+ }
+
+ // Build repo URL: {scheme}://{authority}[/base]/projects/{project}/repos/{repo}
+ // This is everything up to and including the repo name segment
+ var repoUrlPath = path.Substring(0, reposIdx + "/repos/".Length + repoEndSlash);
+
+ repositoryUrl = $"{uri.Scheme}://{uri.Authority}{repoUrlPath}";
+ commitSha = shaSpan.ToString();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error while trying to parse Bitbucket Server SourceLink URL");
+ }
+
+ return false;
+ }
+}
diff --git a/tracer/src/Datadog.Trace/PDBs/SourceLink/CompositeSourceLinkUrlParser.cs b/tracer/src/Datadog.Trace/PDBs/SourceLink/CompositeSourceLinkUrlParser.cs
index 7c0f9a83c509..592422402f7f 100644
--- a/tracer/src/Datadog.Trace/PDBs/SourceLink/CompositeSourceLinkUrlParser.cs
+++ b/tracer/src/Datadog.Trace/PDBs/SourceLink/CompositeSourceLinkUrlParser.cs
@@ -20,6 +20,7 @@ internal sealed class CompositeSourceLinkUrlParser : SourceLinkUrlParser
{
new GitHubSourceLinkUrlParser(),
new BitBucketSourceLinkUrlParser(),
+ new BitBucketServerSourceLinkUrlParser(),
new AzureDevOpsSourceLinkUrlParser(),
new GitLabSourceLinkUrlParser()
};
diff --git a/tracer/src/Datadog.Trace/PDBs/SourceLink/GitHubSourceLinkUrlParser.cs b/tracer/src/Datadog.Trace/PDBs/SourceLink/GitHubSourceLinkUrlParser.cs
index d93c6ccd346f..68c76ece60c7 100644
--- a/tracer/src/Datadog.Trace/PDBs/SourceLink/GitHubSourceLinkUrlParser.cs
+++ b/tracer/src/Datadog.Trace/PDBs/SourceLink/GitHubSourceLinkUrlParser.cs
@@ -14,12 +14,11 @@ namespace Datadog.Trace.Pdb.SourceLink;
internal sealed class GitHubSourceLinkUrlParser : SourceLinkUrlParser
{
///
- /// Extract the git commit sha and repository url from a GitHub SourceLink mapping string.
- /// For example, for the following SourceLink mapping string:
- /// https://raw.githubusercontent.com/DataDog/dd-trace-dotnet/dd35903c688a74b62d1c6a9e4f41371c65704db8/*
- /// It will return:
- /// - commit sha: dd35903c688a74b62d1c6a9e4f41371c65704db8
- /// - repository URL: https://github.com/DataDog/dd-trace-dotnet
+ /// Extract the git commit sha and repository url from a GitHub or GitHub Enterprise SourceLink mapping string.
+ /// Supports three URL forms:
+ /// 1. GitHub.com: https://raw.githubusercontent.com/{owner}/{repo}/{sha}/*
+ /// 2. GHE subdomain isolation: https://raw.{host}/{owner}/{repo}/{sha}/*
+ /// 3. GHE main-host /raw/: https://{host}/raw/{owner}/{repo}/{sha}/*
///
internal override bool TryParseSourceLinkUrl(Uri uri, [NotNullWhen(true)] out string? commitSha, [NotNullWhen(true)] out string? repositoryUrl)
{
@@ -28,52 +27,129 @@ internal override bool TryParseSourceLinkUrl(Uri uri, [NotNullWhen(true)] out st
try
{
- if (uri.Host != "raw.githubusercontent.com")
+ // Case 1: GitHub.com — https://raw.githubusercontent.com/{owner}/{repo}/{sha}/*
+ if (uri.Host.Equals("raw.githubusercontent.com", StringComparison.OrdinalIgnoreCase))
{
- return false;
+ return TryParseStandardPath(uri, "https://github.com", out commitSha, out repositoryUrl);
}
- ReadOnlySpan org = default;
- ReadOnlySpan repo = default;
- ReadOnlySpan sha = default;
- var segmentCount = 0;
+ // Case 2: GHE with subdomain isolation — https://raw.{host}/{owner}/{repo}/{sha}/*
+ if (uri.Host.StartsWith("raw.", StringComparison.OrdinalIgnoreCase) && uri.Host.Length > 4)
+ {
+ var enterpriseHost = uri.Host.Substring(4);
+ return TryParseStandardPath(uri, BuildBaseUrl(uri, enterpriseHost), out commitSha, out repositoryUrl);
+ }
- foreach (var segment in uri.AbsolutePath.SplitIntoSpans('/'))
+ // Case 3: GHE without subdomain isolation — https://{host}/raw/{owner}/{repo}/{sha}/*
+ return TryParseRawPrefixPath(uri, out commitSha, out repositoryUrl);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error while trying to parse GitHub SourceLink URL");
+ }
+
+ return false;
+ }
+
+ ///
+ /// Parses /{owner}/{repo}/{sha}/* (4 segments) and builds repo URL with the given base.
+ /// Used for GitHub.com and GHE with subdomain isolation.
+ ///
+ private static bool TryParseStandardPath(Uri uri, string repoUrlBase, out string? commitSha, out string? repositoryUrl)
+ {
+ commitSha = null;
+ repositoryUrl = null;
+
+ ReadOnlySpan org = default;
+ ReadOnlySpan repo = default;
+ ReadOnlySpan sha = default;
+ var segmentCount = 0;
+
+ foreach (var segment in uri.AbsolutePath.SplitIntoSpans('/'))
+ {
+ ReadOnlySpan span = segment;
+ if (span.IsEmpty)
{
- ReadOnlySpan span = segment;
- if (span.IsEmpty)
- {
- continue;
- }
-
- switch (segmentCount)
- {
- case 0: org = span; break;
- case 1: repo = span; break;
- case 2: sha = span; break;
- }
-
- segmentCount++;
+ continue;
}
- if (segmentCount != 4 || !IsValidCommitSha(sha))
+ switch (segmentCount)
{
- return false;
+ case 0: org = span; break;
+ case 1: repo = span; break;
+ case 2: sha = span; break;
}
+ segmentCount++;
+ }
+
+ if (segmentCount != 4 || !IsValidCommitSha(sha))
+ {
+ return false;
+ }
+
#if NET6_0_OR_GREATER
- repositoryUrl = $"https://github.com/{org}/{repo}";
+ repositoryUrl = $"{repoUrlBase}/{org}/{repo}";
#else
- repositoryUrl = $"https://github.com/{org.ToString()}/{repo.ToString()}";
+ repositoryUrl = $"{repoUrlBase}/{org.ToString()}/{repo.ToString()}";
#endif
- commitSha = sha.ToString();
- return true;
+ commitSha = sha.ToString();
+ return true;
+ }
+
+ ///
+ /// Parses /raw/{owner}/{repo}/{sha}/* (5 segments) for GHE without subdomain isolation.
+ ///
+ private static bool TryParseRawPrefixPath(Uri uri, out string? commitSha, out string? repositoryUrl)
+ {
+ commitSha = null;
+ repositoryUrl = null;
+
+ ReadOnlySpan owner = default;
+ ReadOnlySpan repo = default;
+ ReadOnlySpan sha = default;
+ var segmentCount = 0;
+
+ foreach (var segment in uri.AbsolutePath.SplitIntoSpans('/'))
+ {
+ ReadOnlySpan span = segment;
+ if (span.IsEmpty)
+ {
+ continue;
+ }
+
+ switch (segmentCount)
+ {
+ case 0:
+ if (!span.SequenceEqual("raw".AsSpan()))
+ {
+ return false;
+ }
+
+ break;
+ case 1: owner = span; break;
+ case 2: repo = span; break;
+ case 3: sha = span; break;
+ }
+
+ segmentCount++;
}
- catch (Exception ex)
+
+ if (segmentCount != 5 || !IsValidCommitSha(sha))
{
- Log.Error(ex, "Error while trying to parse GitHub SourceLink URL");
+ return false;
}
- return false;
+ var repoUrlBase = BuildBaseUrl(uri, uri.Host);
+#if NET6_0_OR_GREATER
+ repositoryUrl = $"{repoUrlBase}/{owner}/{repo}";
+#else
+ repositoryUrl = $"{repoUrlBase}/{owner.ToString()}/{repo.ToString()}";
+#endif
+ commitSha = sha.ToString();
+ return true;
}
+
+ private static string BuildBaseUrl(Uri uri, string host)
+ => uri.IsDefaultPort ? $"{uri.Scheme}://{host}" : $"{uri.Scheme}://{host}:{uri.Port}";
}
diff --git a/tracer/src/Datadog.Trace/PDBs/SourceLink/GitLabSourceLinkUrlParser.cs b/tracer/src/Datadog.Trace/PDBs/SourceLink/GitLabSourceLinkUrlParser.cs
index 739aec88afc7..9452d3f714ce 100644
--- a/tracer/src/Datadog.Trace/PDBs/SourceLink/GitLabSourceLinkUrlParser.cs
+++ b/tracer/src/Datadog.Trace/PDBs/SourceLink/GitLabSourceLinkUrlParser.cs
@@ -6,7 +6,6 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
-using Datadog.Trace.Util;
namespace Datadog.Trace.Pdb.SourceLink;
@@ -14,11 +13,9 @@ internal sealed class GitLabSourceLinkUrlParser : SourceLinkUrlParser
{
///
/// Extract the git commit sha and repository url from a GitLab SourceLink mapping string.
- /// For example, for the following SourceLink mapping string:
- /// https://test-gitlab-domain/test-org/test-repo/raw/dd35903c688a74b62d1c6a9e4f41371c65704db8/*
- /// It will return:
- /// - commit sha: dd35903c688a74b62d1c6a9e4f41371c65704db8
- /// - repository URL: https://test-gitlab-domain/test-org/test-repo
+ /// Supports both old and new GitLab URL formats, including nested groups/subgroups:
+ /// GitLab >= 12.0: https://{host}/{group}[/{subgroup}/...]/{repo}/-/raw/{sha}/*
+ /// GitLab < 12.0: https://{host}/{group}[/{subgroup}/...]/{repo}/raw/{sha}/*
///
internal override bool TryParseSourceLinkUrl(Uri uri, [NotNullWhen(true)] out string? commitSha, [NotNullWhen(true)] out string? repositoryUrl)
{
@@ -27,53 +24,58 @@ internal override bool TryParseSourceLinkUrl(Uri uri, [NotNullWhen(true)] out st
try
{
- ReadOnlySpan org = default;
- ReadOnlySpan repo = default;
- ReadOnlySpan sha = default;
- var segmentCount = 0;
+ var path = uri.AbsolutePath;
- foreach (var segment in uri.AbsolutePath.SplitIntoSpans('/'))
+ // Try /-/raw/ first (GitLab >= 12.0), then /raw/ (GitLab < 12.0).
+ // Use LastIndexOf so that repo paths containing "raw" as a segment name don't confuse us.
+ int rawMarkerIndex = path.LastIndexOf("/-/raw/", StringComparison.Ordinal);
+ int repoPathEnd;
+ int afterRawStart;
+
+ if (rawMarkerIndex >= 0)
+ {
+ // /-/raw/ found — new format
+ repoPathEnd = rawMarkerIndex;
+ afterRawStart = rawMarkerIndex + "/-/raw/".Length;
+ }
+ else
{
- ReadOnlySpan span = segment;
- if (span.IsEmpty)
+ rawMarkerIndex = path.LastIndexOf("/raw/", StringComparison.Ordinal);
+ if (rawMarkerIndex <= 0)
{
- continue;
+ // Not found, or /raw/ is at position 0 (which is the GHE pattern, not GitLab)
+ return false;
}
- switch (segmentCount)
- {
- case 0: org = span; break;
- case 1: repo = span; break;
- case 2:
- if (!span.SequenceEqual("raw".AsSpan()))
- {
- return false;
- }
+ repoPathEnd = rawMarkerIndex;
+ afterRawStart = rawMarkerIndex + "/raw/".Length;
+ }
- break;
- case 3: sha = span; break;
- case 4:
- if (!span.SequenceEqual("*".AsSpan()))
- {
- return false;
- }
+ // After the raw marker we expect "{sha}/*"
+ var afterRaw = path.AsSpan().Slice(afterRawStart);
+ var slashIdx = afterRaw.IndexOf('/');
+ if (slashIdx <= 0)
+ {
+ return false;
+ }
- break;
- }
+ var sha = afterRaw.Slice(0, slashIdx);
+ var rest = afterRaw.Slice(slashIdx + 1);
- segmentCount++;
+ if (!rest.SequenceEqual("*".AsSpan()) || !IsValidCommitSha(sha))
+ {
+ return false;
}
- if (segmentCount != 5 || !IsValidCommitSha(sha))
+ // Require at least 2 non-empty segments before the raw marker (group + repo, or group/sub/repo).
+ // After trimming the leading/trailing '/', an inner '/' proves two segments exist.
+ var repoPath = path.AsSpan(0, repoPathEnd).TrimStart('/').TrimEnd('/');
+ if (repoPath.IndexOf('/') <= 0)
{
return false;
}
-#if NET6_0_OR_GREATER
- repositoryUrl = $"{uri.Scheme}://{uri.Authority}/{org}/{repo}";
-#else
- repositoryUrl = $"{uri.Scheme}://{uri.Authority}/{org.ToString()}/{repo.ToString()}";
-#endif
+ repositoryUrl = $"{uri.Scheme}://{uri.Authority}{path.Substring(0, repoPathEnd)}";
commitSha = sha.ToString();
return true;
}
diff --git a/tracer/src/Datadog.Trace/PDBs/SourceLinkInformationExtractor.cs b/tracer/src/Datadog.Trace/PDBs/SourceLinkInformationExtractor.cs
index 42a0c6313b23..898e666b0270 100644
--- a/tracer/src/Datadog.Trace/PDBs/SourceLinkInformationExtractor.cs
+++ b/tracer/src/Datadog.Trace/PDBs/SourceLinkInformationExtractor.cs
@@ -117,7 +117,7 @@ private static bool TryExtractFromAssemblyAttributes(Assembly assembly, [NotNull
case AssemblyInformationalVersionAttribute { InformationalVersion: { } informationalVersion }:
{
var parts = informationalVersion.Split('+');
- if (parts.Length == 2)
+ if (parts.Length == 2 && !StringUtil.IsNullOrEmpty(parts[1]))
{
commitSha = parts[1];
}
diff --git a/tracer/test/Datadog.Trace.ClrProfiler.Managed.Tests/SourceLinkInformationExtractorTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.Managed.Tests/SourceLinkInformationExtractorTests.cs
new file mode 100644
index 000000000000..c0126769cbc5
--- /dev/null
+++ b/tracer/test/Datadog.Trace.ClrProfiler.Managed.Tests/SourceLinkInformationExtractorTests.cs
@@ -0,0 +1,131 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+using System;
+using System.Reflection;
+using System.Reflection.Emit;
+using Datadog.Trace.Pdb;
+using FluentAssertions;
+using Xunit;
+
+#nullable enable
+
+namespace Datadog.Trace.ClrProfiler.Managed.Tests
+{
+ public class SourceLinkInformationExtractorTests
+ {
+ [Theory]
+ [InlineData("1.0.0+abc123", "abc123")] // Valid case - exactly 2 parts
+ [InlineData("2.1.0-beta+def456", "def456")] // Valid case with prerelease
+ public void TryExtractFromAssemblyAttributes_ValidInformationalVersion_ExtractsCommitSha(string informationalVersion, string expectedCommitSha)
+ {
+ var assembly = CreateTestAssembly(repositoryUrl: "https://github.com/test/repo", informationalVersion: informationalVersion);
+
+ var result = SourceLinkInformationExtractor.TryGetSourceLinkInfo(assembly, out var commitSha, out var repositoryUrl);
+
+ result.Should().BeTrue();
+ commitSha.Should().Be(expectedCommitSha);
+ repositoryUrl.Should().Be("https://github.com/test/repo");
+ }
+
+ [Theory]
+ [InlineData("1.0.0")] // No plus signs (parts.Length = 1)
+ [InlineData("1.0.0+build+abc123")] // Multiple plus signs (parts.Length = 3)
+ [InlineData("1.0.0-beta+build+abc123")] // Three parts with prerelease
+ [InlineData("1.0.0+")] // Empty after plus
+ [InlineData("1.0.0+build.meta.data+abc123")] // Multiple metadata with dots
+ [InlineData("1.0.0+20130313144700+abc123")] // Complex build metadata
+ [InlineData("1.0.0-alpha.1+build+abc123")] // With prerelease and multiple plus
+ [InlineData("1.0.0-rc.1+exp.sha.5114f85+abc123")] // Complex prerelease with multiple plus
+ [InlineData("1.0.0+build+meta+abc123")] // Four parts total
+ [InlineData("1.0.0+abc123+def456+ghi789")] // Five parts total
+ public void TryExtractFromAssemblyAttributes_InvalidInformationalVersion_FailsToExtractCommitSha(string informationalVersion)
+ {
+ var assembly = CreateTestAssembly(repositoryUrl: "https://github.com/test/repo", informationalVersion: informationalVersion);
+
+ var result = SourceLinkInformationExtractor.TryGetSourceLinkInfo(assembly, out var commitSha, out var repositoryUrl);
+
+ // Should return false because commit SHA extraction failed (even though repository URL is present)
+ result.Should().BeFalse();
+ commitSha.Should().BeNull();
+ repositoryUrl.Should().BeNull();
+ }
+
+ [Fact]
+ public void TryExtractFromAssemblyAttributes_MissingRepositoryUrl_ReturnsFalse()
+ {
+ var assembly = CreateTestAssembly(repositoryUrl: null, informationalVersion: "1.0.0+abc123");
+
+ var result = SourceLinkInformationExtractor.TryGetSourceLinkInfo(assembly, out var commitSha, out var repositoryUrl);
+
+ result.Should().BeFalse();
+ commitSha.Should().BeNull();
+ repositoryUrl.Should().BeNull();
+ }
+
+ [Fact]
+ public void TryExtractFromAssemblyAttributes_MissingInformationalVersion_ReturnsFalse()
+ {
+ var assembly = CreateTestAssembly(repositoryUrl: "https://github.com/test/repo", informationalVersion: null);
+
+ var result = SourceLinkInformationExtractor.TryGetSourceLinkInfo(assembly, out var commitSha, out var repositoryUrl);
+
+ result.Should().BeFalse();
+ commitSha.Should().BeNull();
+ repositoryUrl.Should().BeNull();
+ }
+
+ [Fact]
+ public void TryExtractFromAssemblyAttributes_EmptyInformationalVersion_ReturnsFalse()
+ {
+ var assembly = CreateTestAssembly(repositoryUrl: "https://github.com/test/repo", informationalVersion: string.Empty);
+
+ var result = SourceLinkInformationExtractor.TryGetSourceLinkInfo(assembly, out var commitSha, out var repositoryUrl);
+
+ result.Should().BeFalse();
+ commitSha.Should().BeNull();
+ repositoryUrl.Should().BeNull();
+ }
+
+ private static Assembly CreateTestAssembly(string? repositoryUrl, string? informationalVersion)
+ {
+ var assemblyName = new AssemblyName($"TestAssembly_{System.Guid.NewGuid():N}");
+ var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
+
+ if (repositoryUrl != null)
+ {
+ var repositoryUrlAttribute = new AssemblyMetadataAttribute("RepositoryUrl", repositoryUrl);
+ assemblyBuilder.SetCustomAttribute(CreateCustomAttributeBuilder(repositoryUrlAttribute));
+ }
+
+ if (!string.IsNullOrEmpty(informationalVersion))
+ {
+ var informationalVersionAttribute = new AssemblyInformationalVersionAttribute(informationalVersion);
+ assemblyBuilder.SetCustomAttribute(CreateCustomAttributeBuilder(informationalVersionAttribute));
+ }
+
+ return assemblyBuilder;
+ }
+
+ private static CustomAttributeBuilder CreateCustomAttributeBuilder(Attribute attribute)
+ {
+ var attributeType = attribute.GetType();
+ var constructor = attributeType.GetConstructors()[0];
+ var constructorArgs = new object[constructor.GetParameters().Length];
+
+ if (attribute is AssemblyMetadataAttribute metadataAttr)
+ {
+ constructorArgs[0] = metadataAttr.Key;
+ constructorArgs[1] = metadataAttr.Value!;
+ }
+ else if (attribute is AssemblyInformationalVersionAttribute versionAttr)
+ {
+ constructorArgs[0] = versionAttr.InformationalVersion;
+ }
+
+ return new CustomAttributeBuilder(constructor, constructorArgs);
+ }
+ }
+}
diff --git a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/AzureDevOpsSourceLinkUrlParserTests.cs b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/AzureDevOpsSourceLinkUrlParserTests.cs
index 2626eaf63097..95625d1042f2 100644
--- a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/AzureDevOpsSourceLinkUrlParserTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/AzureDevOpsSourceLinkUrlParserTests.cs
@@ -19,10 +19,12 @@ public class AzureDevOpsSourceLinkUrlParserTests
private readonly AzureDevOpsSourceLinkUrlParser _parser = new();
[Theory]
+ // Legacy visualstudio.com format
[InlineData(
"https://test.visualstudio.com/test-org/_apis/git/repositories/my-repo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*",
ValidSha,
"https://test.visualstudio.com/test-org/_git/my-repo")]
+ // Modern dev.azure.com format
[InlineData(
"https://dev.azure.com/organisation/project/_apis/git/repositories/repo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*",
ValidSha,
@@ -31,6 +33,21 @@ public class AzureDevOpsSourceLinkUrlParserTests
"https://dev.azure.com/org/proj/_apis/git/repositories/example.shopping.api/items?api-version=1.0&versionType=commit&version=0e4d29442102e6cef1c271025d513c8b2187bcd6&path=/*",
"0e4d29442102e6cef1c271025d513c8b2187bcd6",
"https://dev.azure.com/org/proj/_git/example.shopping.api")]
+ // Azure DevOps Server / TFS on-prem with DefaultCollection
+ [InlineData(
+ "https://tfs-server.localdomain.com/DefaultCollection/TestProject_git/_apis/git/repositories/TestRepo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*",
+ ValidSha,
+ "https://tfs-server.localdomain.com/DefaultCollection/TestProject_git/_git/TestRepo")]
+ // TFS on-prem with virtual directory and collection
+ [InlineData(
+ "https://tfs.example.com/tfs/DefaultCollection/MyProject/_apis/git/repositories/MyRepo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*",
+ ValidSha,
+ "https://tfs.example.com/tfs/DefaultCollection/MyProject/_git/MyRepo")]
+ // On-prem with custom port
+ [InlineData(
+ "https://azdo.internal:8080/MyCollection/MyProject/_apis/git/repositories/MyRepo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*",
+ ValidSha,
+ "https://azdo.internal:8080/MyCollection/MyProject/_git/MyRepo")]
public void TryParseSourceLinkUrl_ValidUrl_ReturnsTrue(string url, string expectedSha, string expectedRepoUrl)
{
var result = _parser.TryParseSourceLinkUrl(new Uri(url), out var commitSha, out var repositoryUrl);
@@ -46,10 +63,9 @@ public void TryParseSourceLinkUrl_ValidUrl_ReturnsTrue(string url, string expect
[InlineData("https://test.visualstudio.com/test-org/_apis/git/repositories/my-repo/items?api-version=1.0&versionType=commit&path=/*")] // missing version=
[InlineData("https://test.visualstudio.com/test-org/_apis/git/repositories/my-repo/items?api-version=1.0&versionType=commit&version=" + ValidSha)] // missing path=/*
[InlineData("https://test.visualstudio.com/test-org/_apis/git/repositories/my-repo/items?api-version=1.0&versionType=commit&version=invalid-sha&path=/*")] // invalid sha
- [InlineData("https://github.com/org/_apis/git/repositories/repo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*")] // unsupported host
- [InlineData("https://test.visualstudio.com/_apis/git/repositories/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*")] // too few path segments (< 5)
[InlineData("https://test.visualstudio.com/test-org/_apis/git/repositories/my-repo/items?api-version=1.0&versionType=commit&version=&path=/*")] // empty version value
[InlineData("https://dev.azure.com/org/proj/_apis/git/repositories/?versionType=commit&version=" + ValidSha + "&path=/*")] // missing repo name (trailing slash, empty segment)
+ [InlineData("https://example.com/_apis/git/repositories/repo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*")] // nothing before _apis (no project)
public void TryParseSourceLinkUrl_InvalidUrl_ReturnsFalse(string url)
{
var result = _parser.TryParseSourceLinkUrl(new Uri(url), out _, out var repositoryUrl);
diff --git a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/BitBucketServerSourceLinkUrlParserTests.cs b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/BitBucketServerSourceLinkUrlParserTests.cs
new file mode 100644
index 000000000000..6f1f8de87d91
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/BitBucketServerSourceLinkUrlParserTests.cs
@@ -0,0 +1,77 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+using System;
+using Datadog.Trace.Pdb.SourceLink;
+using FluentAssertions;
+using Xunit;
+
+namespace Datadog.Trace.Tests.Pdb.SourceLink;
+
+public class BitBucketServerSourceLinkUrlParserTests
+{
+ private const string ValidSha = "dd35903c688a74b62d1c6a9e4f41371c65704db8";
+
+ private readonly BitBucketServerSourceLinkUrlParser _parser = new();
+
+ [Theory]
+ // Bitbucket Server >= 4.7: /projects/{project}/repos/{repo}/raw/*?at={sha}
+ [InlineData(
+ "https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo/raw/*?at=" + ValidSha,
+ ValidSha,
+ "https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo")]
+ // With base path
+ [InlineData(
+ "https://stash.example.com/base/projects/PROJ/repos/my-repo/raw/*?at=" + ValidSha,
+ ValidSha,
+ "https://stash.example.com/base/projects/PROJ/repos/my-repo")]
+ // Bitbucket Server < 4.7: /projects/{project}/repos/{repo}/browse/*?at={sha}&raw
+ [InlineData(
+ "http://stash.mycompany.com:7990/projects/cclcom/repos/myrepo/browse/*?at=" + ValidSha + "&raw",
+ ValidSha,
+ "http://stash.mycompany.com:7990/projects/cclcom/repos/myrepo")]
+ // < 4.7 with base path
+ [InlineData(
+ "https://bitbucket.internal/base/projects/TEAM/repos/core-lib/browse/*?at=" + ValidSha + "&raw",
+ ValidSha,
+ "https://bitbucket.internal/base/projects/TEAM/repos/core-lib")]
+ // < 4.7 with raw flag before at= (order should not matter)
+ [InlineData(
+ "https://stash.mycompany.com/projects/PROJ/repos/repo/browse/*?raw&at=" + ValidSha,
+ ValidSha,
+ "https://stash.mycompany.com/projects/PROJ/repos/repo")]
+ // >= 4.7 with additional query params before at=
+ [InlineData(
+ "https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo/raw/*?limit=100&at=" + ValidSha,
+ ValidSha,
+ "https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo")]
+ public void TryParseSourceLinkUrl_ValidUrl_ReturnsTrue(string url, string expectedSha, string expectedRepoUrl)
+ {
+ var result = _parser.TryParseSourceLinkUrl(new Uri(url), out var commitSha, out var repositoryUrl);
+
+ result.Should().BeTrue();
+ commitSha.Should().Be(expectedSha);
+ repositoryUrl.Should().Be(expectedRepoUrl);
+ }
+
+ [Theory]
+ [InlineData("https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo/raw/*")] // missing at= query parameter
+ [InlineData("https://bitbucket.mycompany.com/repos/myrepo/raw/*?at=" + ValidSha)] // missing /projects/
+ [InlineData("https://bitbucket.mycompany.com/projects/MYPROJ/myrepo/raw/*?at=" + ValidSha)] // missing /repos/
+ [InlineData("https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo/raw/*?at=invalid-sha")] // invalid sha
+ [InlineData("https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo/blob/*?at=" + ValidSha)] // /blob/ instead of /raw/ or /browse/
+ [InlineData("https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo/browse/*?at=" + ValidSha)] // /browse/ without &raw query flag
+ [InlineData("https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo/browse/*?at=" + ValidSha + "&raws=1")] // "raws" is not the standalone "raw" flag
+ public void TryParseSourceLinkUrl_InvalidUrl_ReturnsFalse(string url)
+ {
+ var result = _parser.TryParseSourceLinkUrl(new Uri(url), out var commitSha, out var repositoryUrl);
+
+ result.Should().BeFalse();
+ commitSha.Should().BeNull();
+ repositoryUrl.Should().BeNull();
+ }
+}
diff --git a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/CompositeSourceLinkUrlParserTests.cs b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/CompositeSourceLinkUrlParserTests.cs
index 80a78655440c..2f597016c39b 100644
--- a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/CompositeSourceLinkUrlParserTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/CompositeSourceLinkUrlParserTests.cs
@@ -17,26 +17,66 @@ public class CompositeSourceLinkUrlParserTests
private const string ValidSha = "dd35903c688a74b62d1c6a9e4f41371c65704db8";
[Theory]
+ // GitHub.com
[InlineData(
"https://raw.githubusercontent.com/DataDog/dd-trace-dotnet/" + ValidSha + "/*",
ValidSha,
"https://github.com/DataDog/dd-trace-dotnet")]
+ // GitHub Enterprise — subdomain isolation (raw.{host})
+ [InlineData(
+ "https://raw.github.ecorp.test/org/repo/" + ValidSha + "/*",
+ ValidSha,
+ "https://github.ecorp.test/org/repo")]
+ // GitHub Enterprise — main-host /raw/ form
+ [InlineData(
+ "https://github.ecorp.test/raw/org/repo/" + ValidSha + "/*",
+ ValidSha,
+ "https://github.ecorp.test/org/repo")]
+ // Bitbucket Cloud (API 2.0)
[InlineData(
"https://api.bitbucket.org/2.0/repositories/test-org/test-repo/src/" + ValidSha + "/*",
ValidSha,
"https://bitbucket.org/test-org/test-repo")]
+ // Bitbucket Server >= 4.7
+ [InlineData(
+ "https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo/raw/*?at=" + ValidSha,
+ ValidSha,
+ "https://bitbucket.mycompany.com/projects/MYPROJ/repos/myrepo")]
+ // Bitbucket Server < 4.7
+ [InlineData(
+ "http://stash.mycompany.com:7990/projects/cclcom/repos/myrepo/browse/*?at=" + ValidSha + "&raw",
+ ValidSha,
+ "http://stash.mycompany.com:7990/projects/cclcom/repos/myrepo")]
+ // Azure DevOps — visualstudio.com
[InlineData(
"https://test.visualstudio.com/test-org/_apis/git/repositories/my-repo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*",
ValidSha,
"https://test.visualstudio.com/test-org/_git/my-repo")]
+ // Azure DevOps — dev.azure.com
[InlineData(
"https://dev.azure.com/org/proj/_apis/git/repositories/repo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*",
ValidSha,
"https://dev.azure.com/org/proj/_git/repo")]
+ // Azure DevOps Server / TFS on-prem
+ [InlineData(
+ "https://tfs-server.localdomain.com/DefaultCollection/TestProject/_apis/git/repositories/TestRepo/items?api-version=1.0&versionType=commit&version=" + ValidSha + "&path=/*",
+ ValidSha,
+ "https://tfs-server.localdomain.com/DefaultCollection/TestProject/_git/TestRepo")]
+ // GitLab < 12.0
[InlineData(
"https://gitlab.com/test-org/test-repo/raw/" + ValidSha + "/*",
ValidSha,
"https://gitlab.com/test-org/test-repo")]
+ // GitLab >= 12.0
+ [InlineData(
+ "https://gitlab.com/test-org/test-repo/-/raw/" + ValidSha + "/*",
+ ValidSha,
+ "https://gitlab.com/test-org/test-repo")]
+ // GitLab with nested groups
+ [InlineData(
+ "https://gitlab.com/group/subgroup/repo/-/raw/" + ValidSha + "/*",
+ ValidSha,
+ "https://gitlab.com/group/subgroup/repo")]
public void TryParseSourceLinkUrl_ValidUrl_RoutesToCorrectParser(string url, string expectedSha, string expectedRepoUrl)
{
var result = CompositeSourceLinkUrlParser.Instance.TryParseSourceLinkUrl(new Uri(url), out var commitSha, out var repositoryUrl);
diff --git a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/GitHubSourceLinkUrlParserTests.cs b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/GitHubSourceLinkUrlParserTests.cs
index 0624ec33cdb8..7d1194806a27 100644
--- a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/GitHubSourceLinkUrlParserTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/GitHubSourceLinkUrlParserTests.cs
@@ -31,6 +31,24 @@ public class GitHubSourceLinkUrlParserTests
"https://raw.githubusercontent.com/some.org/some.repo-name/" + ValidSha + "/*",
ValidSha,
"https://github.com/some.org/some.repo-name")]
+ // GitHub Enterprise with subdomain isolation (raw.{host} form)
+ [InlineData(
+ "https://raw.github.ecorp.example.com/taggac/vsphere-automation-sdk-.net/" + ValidSha + "/*",
+ ValidSha,
+ "https://github.ecorp.example.com/taggac/vsphere-automation-sdk-.net")]
+ [InlineData(
+ "https://raw.ghe.internal/my-org/my-repo/" + ValidSha + "/*",
+ ValidSha,
+ "https://ghe.internal/my-org/my-repo")]
+ // GitHub Enterprise without subdomain isolation (/raw/ path form)
+ [InlineData(
+ "https://github.ecorp.test/raw/taggac/vsphere-automation-sdk-.net/" + ValidSha + "/*",
+ ValidSha,
+ "https://github.ecorp.test/taggac/vsphere-automation-sdk-.net")]
+ [InlineData(
+ "https://ghe.internal:8443/raw/my-org/my-repo/" + ValidSha + "/*",
+ ValidSha,
+ "https://ghe.internal:8443/my-org/my-repo")]
public void TryParseSourceLinkUrl_ValidUrl_ReturnsTrue(string url, string expectedSha, string expectedRepoUrl)
{
var result = _parser.TryParseSourceLinkUrl(new Uri(url), out var commitSha, out var repositoryUrl);
@@ -41,12 +59,17 @@ public void TryParseSourceLinkUrl_ValidUrl_ReturnsTrue(string url, string expect
}
[Theory]
- [InlineData("https://raw.example.com/DataDog/dd-trace-dotnet/" + ValidSha + "/*")] // wrong host
[InlineData("https://raw.githubusercontent.com/DataDog/dd-trace-dotnet/*")] // missing sha
[InlineData("https://raw.githubusercontent.com/DataDog/dd-trace-dotnet/" + ValidSha + "/extra/*")] // too many segments
[InlineData("https://raw.githubusercontent.com/DataDog/dd-trace-dotnet/abc123/*")] // sha too short
[InlineData("https://raw.githubusercontent.com/DataDog/dd-trace-dotnet/zz35903c688a74b62d1c6a9e4f41371c65704db!/*")] // non-hex chars
[InlineData("https://raw.githubusercontent.com/")] // empty path
+ [InlineData("https://raw./owner/repo/" + ValidSha + "/*")] // raw. with empty enterprise host
+ [InlineData("https://raw.ghe.internal/owner/repo/" + ValidSha)] // GHE subdomain isolation with too few segments (no trailing /*)
+ [InlineData("https://raw.ghe.internal/owner/" + ValidSha + "/*")] // GHE subdomain isolation with too few segments (3 instead of 4)
+ [InlineData("https://example.com/raw/owner/repo/not-a-sha/*")] // GHE /raw/ form with invalid sha
+ [InlineData("https://example.com/raw/owner/" + ValidSha + "/*")] // GHE /raw/ form with missing repo (4 segments, not 5)
+ [InlineData("https://example.com/notraw/owner/repo/" + ValidSha + "/*")] // /notraw/ is not /raw/
public void TryParseSourceLinkUrl_InvalidUrl_ReturnsFalse(string url)
{
var result = _parser.TryParseSourceLinkUrl(new Uri(url), out var commitSha, out var repositoryUrl);
diff --git a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/GitLabSourceLinkUrlParserTests.cs b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/GitLabSourceLinkUrlParserTests.cs
index 946af621e90a..9c8c03a69d53 100644
--- a/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/GitLabSourceLinkUrlParserTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Pdb/SourceLink/GitLabSourceLinkUrlParserTests.cs
@@ -19,6 +19,7 @@ public class GitLabSourceLinkUrlParserTests
private readonly GitLabSourceLinkUrlParser _parser = new();
[Theory]
+ // GitLab < 12.0 — /{group}/{repo}/raw/{sha}/*
[InlineData(
"https://gitlab.com/test-org/test-repo/raw/" + ValidSha + "/*",
ValidSha,
@@ -31,6 +32,33 @@ public class GitLabSourceLinkUrlParserTests
"https://gitlab.example.com:8443/org/repo/raw/" + ValidSha + "/*",
ValidSha,
"https://gitlab.example.com:8443/org/repo")]
+ // GitLab >= 12.0 — /{group}/{repo}/-/raw/{sha}/*
+ [InlineData(
+ "https://gitlab.com/test-org/test-repo/-/raw/" + ValidSha + "/*",
+ ValidSha,
+ "https://gitlab.com/test-org/test-repo")]
+ [InlineData(
+ "https://gitlab.example.com/example/example-dotnet-source-link/-/raw/" + ValidSha + "/*",
+ ValidSha,
+ "https://gitlab.example.com/example/example-dotnet-source-link")]
+ // GitLab nested groups/subgroups (< 12.0 format)
+ [InlineData(
+ "https://gitlab.com/group/subgroup/repo/raw/" + ValidSha + "/*",
+ ValidSha,
+ "https://gitlab.com/group/subgroup/repo")]
+ [InlineData(
+ "https://gitlab.com/group/sub1/sub2/repo/raw/" + ValidSha + "/*",
+ ValidSha,
+ "https://gitlab.com/group/sub1/sub2/repo")]
+ // GitLab nested groups/subgroups (>= 12.0 format)
+ [InlineData(
+ "https://gitlab.com/group/subgroup/repo/-/raw/" + ValidSha + "/*",
+ ValidSha,
+ "https://gitlab.com/group/subgroup/repo")]
+ [InlineData(
+ "https://gitlab.com/group/sub1/sub2/repo/-/raw/" + ValidSha + "/*",
+ ValidSha,
+ "https://gitlab.com/group/sub1/sub2/repo")]
public void TryParseSourceLinkUrl_ValidUrl_ReturnsTrue(string url, string expectedSha, string expectedRepoUrl)
{
var result = _parser.TryParseSourceLinkUrl(new Uri(url), out var commitSha, out var repositoryUrl);
@@ -41,11 +69,12 @@ public void TryParseSourceLinkUrl_ValidUrl_ReturnsTrue(string url, string expect
}
[Theory]
- [InlineData("https://gitlab.com/test-org/raw/" + ValidSha + "/*")] // too few segments (4 instead of 5)
- [InlineData("https://gitlab.com/test-org/sub/test-repo/raw/" + ValidSha + "/*")] // too many segments
- [InlineData("https://gitlab.com/test-org/test-repo/blob/" + ValidSha + "/*")] // segments[2] != "raw"
- [InlineData("https://gitlab.com/test-org/test-repo/raw/" + ValidSha + "/specific-file")] // segments[4] != "*"
+ [InlineData("https://gitlab.com/test-org/raw/" + ValidSha + "/*")] // only one segment before /raw/ (need at least 2)
+ [InlineData("https://gitlab.com/test-org/test-repo/blob/" + ValidSha + "/*")] // "blob" is not "raw" or "-/raw"
+ [InlineData("https://gitlab.com/test-org/test-repo/raw/" + ValidSha + "/specific-file")] // trailing segment != "*"
[InlineData("https://gitlab.com/test-org/test-repo/raw/invalid-sha/*")] // invalid sha
+ [InlineData("https://gitlab.com/test-org/test-repo/-/raw/invalid-sha/*")] // invalid sha with new format
+ [InlineData("https://gitlab.com/test-org/test-repo/-/raw/" + ValidSha + "/specific-file")] // trailing segment != "*" with new format
public void TryParseSourceLinkUrl_InvalidUrl_ReturnsFalse(string url)
{
var result = _parser.TryParseSourceLinkUrl(new Uri(url), out var commitSha, out var repositoryUrl);