Skip to content

Commit 1b424fc

Browse files
Feature: GitWeb Source Link provider
GitWeb only currently supports SSH repository URLs. #505
1 parent 39c946a commit 1b424fc

28 files changed

+844
-10
lines changed

SourceLink.sln

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.AzureD
5454
EndProject
5555
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.AzureDevOpsServer.Git.UnitTests", "src\SourceLink.AzureDevOpsServer.Git.UnitTests\Microsoft.SourceLink.AzureDevOpsServer.Git.UnitTests.csproj", "{79371F26-FB84-408D-A4A1-B142B247C288}"
5656
EndProject
57+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitWeb", "src\SourceLink.GitWeb\Microsoft.SourceLink.GitWeb.csproj", "{C78DD3EF-9D20-4E00-8237-E871BB53F840}"
58+
EndProject
59+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitWeb.UnitTests", "src\SourceLink.GitWeb.UnitTests\Microsoft.SourceLink.GitWeb.UnitTests.csproj", "{50503A43-08C0-493B-B8CC-F368983644C1}"
60+
EndProject
5761
Global
5862
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5963
Debug|Any CPU = Debug|Any CPU
@@ -136,6 +140,14 @@ Global
136140
{79371F26-FB84-408D-A4A1-B142B247C288}.Debug|Any CPU.Build.0 = Debug|Any CPU
137141
{79371F26-FB84-408D-A4A1-B142B247C288}.Release|Any CPU.ActiveCfg = Release|Any CPU
138142
{79371F26-FB84-408D-A4A1-B142B247C288}.Release|Any CPU.Build.0 = Release|Any CPU
143+
{C78DD3EF-9D20-4E00-8237-E871BB53F840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
144+
{C78DD3EF-9D20-4E00-8237-E871BB53F840}.Debug|Any CPU.Build.0 = Debug|Any CPU
145+
{C78DD3EF-9D20-4E00-8237-E871BB53F840}.Release|Any CPU.ActiveCfg = Release|Any CPU
146+
{C78DD3EF-9D20-4E00-8237-E871BB53F840}.Release|Any CPU.Build.0 = Release|Any CPU
147+
{50503A43-08C0-493B-B8CC-F368983644C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
148+
{50503A43-08C0-493B-B8CC-F368983644C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
149+
{50503A43-08C0-493B-B8CC-F368983644C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
150+
{50503A43-08C0-493B-B8CC-F368983644C1}.Release|Any CPU.Build.0 = Release|Any CPU
139151
EndGlobalSection
140152
GlobalSection(SolutionProperties) = preSolution
141153
HideSolutionNode = FALSE

src/Common/TranslateRepositoryUrlGitTask.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
2+
// License.txt in the project root for license information.
23

34
using System;
45
using System.Collections.Generic;
@@ -48,13 +49,13 @@ private void ExecuteImpl()
4849
}
4950

5051
static bool isMatchingHostUri(Uri hostUri, Uri uri)
51-
=> uri.GetHost().Equals(hostUri.GetHost(), StringComparison.OrdinalIgnoreCase) ||
52+
=> uri.GetHost().Equals(hostUri.GetHost(), StringComparison.OrdinalIgnoreCase) ||
5253
uri.GetHost().EndsWith("." + hostUri.GetHost(), StringComparison.OrdinalIgnoreCase);
5354

5455
// only need to translate valid ssh URLs that match one of our hosts:
5556
string? translate(string? url)
5657
{
57-
if (Uri.TryCreate(url, UriKind.Absolute, out var uri) &&
58+
if (Uri.TryCreate(url, UriKind.Absolute, out var uri) &&
5859
hostUris.Any(h => isMatchingHostUri(h, uri)))
5960
{
6061
return (uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? TranslateHttpUrl(uri) :
@@ -66,7 +67,16 @@ static bool isMatchingHostUri(Uri hostUri, Uri uri)
6667
return url;
6768
}
6869

69-
TranslatedRepositoryUrl = translate(RepositoryUrl);
70+
try
71+
{
72+
TranslatedRepositoryUrl = translate(RepositoryUrl);
73+
}
74+
catch (NotSupportedException e)
75+
{
76+
Log.LogError(e.Message);
77+
return;
78+
}
79+
7080
TranslatedSourceRoots = SourceRoots;
7181

7282
if (TranslatedSourceRoots != null)
@@ -78,12 +88,23 @@ static bool isMatchingHostUri(Uri hostUri, Uri uri)
7888
continue;
7989
}
8090

81-
// Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata stores the value as specified.
82-
// When initializing the URL metadata from git information we msbuild-escaped the URL to preserve any URL escapes in it.
83-
// Here, GetMetadata unescapes the msbuild escapes, then we translate the URL and finally msbuild-escape
84-
// the resulting URL to preserve any URL escapes.
85-
sourceRoot.SetMetadata(Names.SourceRoot.ScmRepositoryUrl,
86-
Evaluation.ProjectCollection.Escape(translate(sourceRoot.GetMetadata(Names.SourceRoot.ScmRepositoryUrl))));
91+
string? translatedUrl;
92+
try
93+
{
94+
translatedUrl = translate(sourceRoot.GetMetadata(Names.SourceRoot.ScmRepositoryUrl));
95+
}
96+
catch (NotSupportedException e)
97+
{
98+
Log.LogError(e.Message);
99+
continue;
100+
}
101+
102+
// Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata
103+
// stores the value as specified. When initializing the URL metadata from git
104+
// information we msbuild-escaped the URL to preserve any URL escapes in it.
105+
// Here, GetMetadata unescapes the msbuild escapes, then we translate the URL
106+
// and finally msbuild-escape the resulting URL to preserve any URL escapes.
107+
sourceRoot.SetMetadata(Names.SourceRoot.ScmRepositoryUrl, Evaluation.ProjectCollection.Escape(translatedUrl));
87108
}
88109
}
89110
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
2+
// License.txt in the project root for license information.
3+
4+
using Microsoft.Build.Tasks.SourceControl;
5+
using System;
6+
using System.IO;
7+
using TestUtilities;
8+
9+
namespace Microsoft.SourceLink.IntegrationTests
10+
{
11+
public class GitWebTests : DotNetSdkTestBase
12+
{
13+
public GitWebTests()
14+
: base("Microsoft.SourceLink.GitWeb")
15+
{
16+
}
17+
18+
[ConditionalFact(typeof(DotNetSdkAvailable))]
19+
public void FullValidation_Ssh()
20+
{
21+
// Test non-ascii characters and escapes in the URL. Escaped URI reserved characters
22+
// should remain escaped, non-reserved characters unescaped in the results.
23+
var repoUrl = $"ssh://git@噸.com/test-%72epo\u1234%24%2572%2F.git";
24+
var repoName = "test-repo\u1234%24%2572%2F.git";
25+
26+
var repo = GitUtilities.CreateGitRepositoryWithSingleCommit(ProjectDir.Path, new[] { ProjectFileName }, repoUrl);
27+
var commitSha = repo.Head.Tip.Sha;
28+
29+
VerifyValues(
30+
customProps: @"
31+
<PropertyGroup>
32+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
33+
</PropertyGroup>
34+
<ItemGroup>
35+
<SourceLinkGitWebHost Include='噸.com' ContentUrl='https://噸.com/gitweb'/>
36+
</ItemGroup>
37+
",
38+
customTargets: "",
39+
targets: new[]
40+
{
41+
"Build", "Pack"
42+
},
43+
expressions: new[]
44+
{
45+
"@(SourceRoot)",
46+
"@(SourceRoot->'%(SourceLinkUrl)')",
47+
"$(SourceLink)",
48+
"$(PrivateRepositoryUrl)",
49+
"$(RepositoryUrl)"
50+
},
51+
expectedResults: new[]
52+
{
53+
ProjectSourceRoot,
54+
$"https://噸.com/gitweb/?p={repoName};a=blob_plain;hb={commitSha};f=*",
55+
s_relativeSourceLinkJsonPath,
56+
$"ssh://git@噸.com/{repoName}",
57+
$"ssh://git@噸.com/{repoName}"
58+
});
59+
60+
AssertEx.AreEqual(
61+
$@"{{""documents"":{{""{ProjectSourceRoot.Replace(@"\", @"\\")}*"":""https://噸.com/gitweb/?p={repoName};a=blob_plain;hb={commitSha};f=*""}}}}",
62+
File.ReadAllText(Path.Combine(ProjectDir.Path, s_relativeSourceLinkJsonPath)));
63+
64+
TestUtilities.ValidateAssemblyInformationalVersion(
65+
Path.Combine(ProjectDir.Path, s_relativeOutputFilePath),
66+
"1.0.0+" + commitSha);
67+
68+
TestUtilities.ValidateNuSpecRepository(
69+
Path.Combine(ProjectDir.Path, s_relativePackagePath),
70+
type: "git",
71+
commit: commitSha,
72+
url: $"ssh://git@噸.com/{repoName}");
73+
}
74+
75+
[ConditionalFact(typeof(DotNetSdkAvailable))]
76+
public void Issues_error_on_git_url()
77+
{
78+
var repoUrl = "git://噸.com/invalid_url_protocol.git";
79+
var repo = GitUtilities.CreateGitRepositoryWithSingleCommit(ProjectDir.Path, new[] { ProjectFileName }, repoUrl);
80+
var commitSha = repo.Head.Tip.Sha;
81+
82+
VerifyValues(
83+
customProps: @"
84+
<PropertyGroup>
85+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
86+
</PropertyGroup>
87+
<ItemGroup>
88+
<SourceLinkGitWebHost Include='噸.com' ContentUrl='https://噸.com/gitweb'/>
89+
</ItemGroup>
90+
",
91+
customTargets: "",
92+
targets: new[]
93+
{
94+
"Build", "Pack"
95+
},
96+
expressions: Array.Empty<string>(),
97+
expectedErrors: new[]{
98+
string.Format(GitWeb.Resources.RepositoryUrlIsNotSupportedByProvider, "GIT")
99+
});
100+
}
101+
102+
[ConditionalFact(typeof(DotNetSdkAvailable))]
103+
public void Issues_error_on_https_url()
104+
{
105+
var repoUrl = "https://噸.com/invalid_url_protocol.git";
106+
var repo = GitUtilities.CreateGitRepositoryWithSingleCommit(ProjectDir.Path, new[] { ProjectFileName }, repoUrl);
107+
var commitSha = repo.Head.Tip.Sha;
108+
109+
VerifyValues(
110+
customProps: @"
111+
<PropertyGroup>
112+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
113+
</PropertyGroup>
114+
<ItemGroup>
115+
<SourceLinkGitWebHost Include='噸.com' ContentUrl='https://噸.com/gitweb'/>
116+
</ItemGroup>
117+
",
118+
customTargets: "",
119+
targets: new[]
120+
{
121+
"Build", "Pack"
122+
},
123+
expressions: Array.Empty<string>(),
124+
expectedErrors: new[]
125+
{
126+
string.Format(GitWeb.Resources.RepositoryUrlIsNotSupportedByProvider, "HTTP")
127+
});
128+
}
129+
}
130+
}

src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<ProjectReference Include="..\Microsoft.Build.Tasks.Git\Microsoft.Build.Tasks.Git.csproj" />
88
<ProjectReference Include="..\SourceLink.Common\Microsoft.SourceLink.Common.csproj" />
99
<ProjectReference Include="..\SourceLink.GitHub\Microsoft.SourceLink.GitHub.csproj" />
10+
<ProjectReference Include="..\SourceLink.GitWeb\Microsoft.SourceLink.GitWeb.csproj" />
1011
<ProjectReference Include="..\TestUtilities\TestUtilities.csproj" />
1112
</ItemGroup>
1213
</Project>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
2+
// License.txt in the project root for license information.
3+
using Microsoft.Build.Tasks.SourceControl;
4+
using TestUtilities;
5+
using Xunit;
6+
using static TestUtilities.KeyValuePairUtils;
7+
8+
namespace Microsoft.SourceLink.GitWeb.UnitTests
9+
{
10+
public class GetSourceLinkUrlTests
11+
{
12+
[Fact]
13+
public void EmptyHosts()
14+
{
15+
var engine = new MockEngine();
16+
17+
var task = new GetSourceLinkUrl()
18+
{
19+
BuildEngine = engine,
20+
SourceRoot = new MockItem("x", KVP("RepositoryUrl", "http://abc.com"), KVP("SourceControl", "git")),
21+
};
22+
23+
bool result = task.Execute();
24+
25+
AssertEx.AssertEqualToleratingWhitespaceDifferences(
26+
"ERROR : " + string.Format(CommonResources.AtLeastOneRepositoryHostIsRequired, "SourceLinkGitWebHost", "GitWeb"), engine.Log);
27+
28+
Assert.False(result);
29+
}
30+
31+
[Theory]
32+
[InlineData("", "")]
33+
[InlineData("", "/")]
34+
[InlineData("/", "")]
35+
[InlineData("/", "/")]
36+
public void BuildSourceLinkUrl(string s1, string s2)
37+
{
38+
var engine = new MockEngine();
39+
40+
var task = new GetSourceLinkUrl()
41+
{
42+
BuildEngine = engine,
43+
SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "ssh://[email protected]/root_dir_name/sub_dirs/reponame.git" + s1), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
44+
Hosts = new[]
45+
{
46+
// NOTE: i don't know what the spec parameter is for. but for all the other
47+
// tests like this for various providers it is the domain of the Repo URL.
48+
new MockItem("src.intranet.company.com", KVP("ContentUrl", "https://src.intranet.company.com/gitweb" + s2)),
49+
}
50+
};
51+
52+
bool result = task.Execute();
53+
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
54+
AssertEx.AreEqual("https://src.intranet.company.com/gitweb/?p=root_dir_name/sub_dirs/reponame.git;a=blob_plain;hb=0123456789abcdefABCDEF000000000000000000;f=*", task.SourceLinkUrl);
55+
Assert.True(result);
56+
}
57+
}
58+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
4+
</PropertyGroup>
5+
<ItemGroup>
6+
<ProjectReference Include="..\SourceLink.GitWeb\Microsoft.SourceLink.GitWeb.csproj" />
7+
<ProjectReference Include="..\TestUtilities\TestUtilities.csproj" />
8+
</ItemGroup>
9+
</Project>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
2+
// License.txt in the project root for license information.
3+
using System.Linq;
4+
using TestUtilities;
5+
using Xunit;
6+
using static TestUtilities.KeyValuePairUtils;
7+
8+
namespace Microsoft.SourceLink.GitWeb.UnitTests
9+
{
10+
public class TranslateRepositoryUrlsTests
11+
{
12+
[Fact]
13+
public void Translate()
14+
{
15+
var engine = new MockEngine();
16+
17+
var task = new TranslateRepositoryUrls()
18+
{
19+
BuildEngine = engine,
20+
RepositoryUrl = "ssh://[email protected]/root_dir_name/sub_dirs/reponame.git",
21+
IsSingleProvider = true,
22+
SourceRoots = new[]
23+
{
24+
new MockItem("/1/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://[email protected]/root_dir_name/sub_dirs/reponame.git")),
25+
new MockItem("/2/", KVP("SourceControl", "tfvc"), KVP("ScmRepositoryUrl", "ssh://[email protected]/root_dir_name/sub_dirs/reponame.git")),
26+
new MockItem("/2/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://[email protected]/root_dir_name/sub_dirs/reponame.git")),
27+
new MockItem("/2/", KVP("SourceControl", "tfvc"), KVP("ScmRepositoryUrl", "ssh://[email protected]/root_dir_name/sub_dirs/reponame.git")),
28+
},
29+
Hosts = new[]
30+
{
31+
new MockItem("src.intranet.company1.com")
32+
}
33+
};
34+
35+
bool result = task.Execute();
36+
AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
37+
38+
AssertEx.AreEqual("ssh://[email protected]/root_dir_name/sub_dirs/reponame.git", task.TranslatedRepositoryUrl);
39+
40+
AssertEx.Equal(new[]
41+
{
42+
"ssh://[email protected]/root_dir_name/sub_dirs/reponame.git",
43+
"ssh://[email protected]/root_dir_name/sub_dirs/reponame.git",
44+
"ssh://[email protected]/root_dir_name/sub_dirs/reponame.git",
45+
"ssh://[email protected]/root_dir_name/sub_dirs/reponame.git",
46+
}, task.TranslatedSourceRoots.Select(r => r.GetMetadata("ScmRepositoryUrl")));
47+
48+
Assert.True(result);
49+
}
50+
}
51+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
2+
// License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Build.Framework;
6+
using Microsoft.Build.Tasks.SourceControl;
7+
8+
namespace Microsoft.SourceLink.GitWeb
9+
{
10+
/// <summary>
11+
/// The task calculates SourceLink URL for a given SourceRoot. If the SourceRoot is associated
12+
/// with a git repository with a recognized domain the <see cref="SourceLinkUrl"/> output
13+
/// property is set to the content URL corresponding to the domain, otherwise it is set to
14+
/// string "N/A".
15+
/// </summary>
16+
public sealed class GetSourceLinkUrl : GetSourceLinkUrlGitTask
17+
{
18+
protected override string HostsItemGroupName => "SourceLinkGitWebHost";
19+
protected override string ProviderDisplayName => "GitWeb";
20+
21+
protected override Uri GetDefaultContentUriFromHostUri(string authority, Uri gitUri)
22+
=> new Uri($"https://{authority}/gitweb", UriKind.Absolute);
23+
24+
protected override string BuildSourceLinkUrl(Uri contentUri, Uri gitUri, string relativeUrl, string revisionId, ITaskItem? hostItem)
25+
{
26+
var trimLeadingSlash = relativeUrl.TrimStart('/');
27+
var trimmedContentUrl = contentUri.ToString().TrimEnd('/', '\\');
28+
29+
// p = project/path
30+
// a = action
31+
// hb = SHA/revision
32+
// f = repo file path
33+
var gitwebRawUrl = UriUtilities.Combine(trimmedContentUrl, $"?p={trimLeadingSlash}.git;a=blob_plain;hb={revisionId};f=*");
34+
return gitwebRawUrl;
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)