diff --git a/SourceLink.sln b/SourceLink.sln
index 49098d14..b185660e 100644
--- a/SourceLink.sln
+++ b/SourceLink.sln
@@ -54,6 +54,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.AzureD
EndProject
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}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitWeb", "src\SourceLink.GitWeb\Microsoft.SourceLink.GitWeb.csproj", "{C78DD3EF-9D20-4E00-8237-E871BB53F840}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitWeb.UnitTests", "src\SourceLink.GitWeb.UnitTests\Microsoft.SourceLink.GitWeb.UnitTests.csproj", "{50503A43-08C0-493B-B8CC-F368983644C1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -136,6 +140,14 @@ Global
{79371F26-FB84-408D-A4A1-B142B247C288}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79371F26-FB84-408D-A4A1-B142B247C288}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79371F26-FB84-408D-A4A1-B142B247C288}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C78DD3EF-9D20-4E00-8237-E871BB53F840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C78DD3EF-9D20-4E00-8237-E871BB53F840}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C78DD3EF-9D20-4E00-8237-E871BB53F840}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C78DD3EF-9D20-4E00-8237-E871BB53F840}.Release|Any CPU.Build.0 = Release|Any CPU
+ {50503A43-08C0-493B-B8CC-F368983644C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {50503A43-08C0-493B-B8CC-F368983644C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {50503A43-08C0-493B-B8CC-F368983644C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {50503A43-08C0-493B-B8CC-F368983644C1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Common/CommonResources.resx b/src/Common/CommonResources.resx
index 876d8353..5329649d 100644
--- a/src/Common/CommonResources.resx
+++ b/src/Common/CommonResources.resx
@@ -141,4 +141,10 @@
Unable to determine repository url, the source code won't be available via source link.
+
+ The provided url '{0}' is not a complete URI
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
\ No newline at end of file
diff --git a/src/Common/TranslateRepositoryUrlGitTask.cs b/src/Common/TranslateRepositoryUrlGitTask.cs
index b87b2d70..56cbccd9 100644
--- a/src/Common/TranslateRepositoryUrlGitTask.cs
+++ b/src/Common/TranslateRepositoryUrlGitTask.cs
@@ -33,6 +33,9 @@ public class TranslateRepositoryUrlsGitTask : Task
protected virtual string? TranslateHttpUrl(Uri uri)
=> uri.GetScheme() + "://" + uri.GetAuthority() + uri.GetPathAndQuery();
+ protected virtual bool IsProtocolSupportedByProvider(string protocol)
+ => true;
+
public override bool Execute()
{
ExecuteImpl();
@@ -47,26 +50,45 @@ private void ExecuteImpl()
return;
}
+ static bool getUri(string? url, out Uri uri) => Uri.TryCreate(url, UriKind.Absolute, out uri);
+
static bool isMatchingHostUri(Uri hostUri, Uri uri)
- => uri.GetHost().Equals(hostUri.GetHost(), StringComparison.OrdinalIgnoreCase) ||
+ => uri.GetHost().Equals(hostUri.GetHost(), StringComparison.OrdinalIgnoreCase) ||
uri.GetHost().EndsWith("." + hostUri.GetHost(), StringComparison.OrdinalIgnoreCase);
// only need to translate valid ssh URLs that match one of our hosts:
- string? translate(string? url)
+ string translate(Uri uri)
{
- if (Uri.TryCreate(url, UriKind.Absolute, out var uri) &&
- hostUris.Any(h => isMatchingHostUri(h, uri)))
+ if(hostUris.Any(h => isMatchingHostUri(h, uri)))
{
return (uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? TranslateHttpUrl(uri) :
uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ? TranslateHttpUrl(uri) :
uri.Scheme.Equals("ssh", StringComparison.OrdinalIgnoreCase) ? TranslateSshUrl(uri) :
- uri.Scheme.Equals("git", StringComparison.OrdinalIgnoreCase) ? TranslateGitUrl(uri) : null) ?? url;
+ uri.Scheme.Equals("git", StringComparison.OrdinalIgnoreCase) ? TranslateGitUrl(uri) : null) ?? uri.OriginalString;
}
- return url;
+ return uri.OriginalString;
+ }
+
+ var providerName = this.GetType().Namespace;
+ if(getUri(RepositoryUrl, out var repoUrlUri))
+ {
+ if(!IsProtocolSupportedByProvider(repoUrlUri.Scheme))
+ {
+ // TODO should this be an error or a warning?
+ Log.LogError(CommonResources.RepositoryUrlIsNotSupportedByProvider, providerName, repoUrlUri.Scheme);
+ return;
+ }
+
+ TranslatedRepositoryUrl = translate(repoUrlUri);
+ }
+ else
+ {
+ // preserve previous behavior of using the URL as is if URI cannot be parsed.
+ TranslatedRepositoryUrl = RepositoryUrl;
+ Log.LogWarning(CommonResources.RepositoryUrlIsNotComplete, RepositoryUrl);
}
- TranslatedRepositoryUrl = translate(RepositoryUrl);
TranslatedSourceRoots = SourceRoots;
if (TranslatedSourceRoots != null)
@@ -78,12 +100,35 @@ static bool isMatchingHostUri(Uri hostUri, Uri uri)
continue;
}
- // Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata stores the value as specified.
- // When initializing the URL metadata from git information we msbuild-escaped the URL to preserve any URL escapes in it.
- // Here, GetMetadata unescapes the msbuild escapes, then we translate the URL and finally msbuild-escape
- // the resulting URL to preserve any URL escapes.
- sourceRoot.SetMetadata(Names.SourceRoot.ScmRepositoryUrl,
- Evaluation.ProjectCollection.Escape(translate(sourceRoot.GetMetadata(Names.SourceRoot.ScmRepositoryUrl))));
+ var scmRepoUrl = sourceRoot.GetMetadata(Names.SourceRoot.ScmRepositoryUrl);
+ if (getUri(scmRepoUrl, out var scmRepoUri))
+ {
+ if (!IsProtocolSupportedByProvider(scmRepoUri.Scheme))
+ {
+ // TODO should this be an error or a warning?
+ Log.LogError(CommonResources.RepositoryUrlIsNotSupportedByProvider, providerName, scmRepoUri.Scheme);
+ continue;
+ }
+
+ setScmRepoUrlMetaData(translate(scmRepoUri));
+ }
+ else
+ {
+ // preserve previous behavior of using the URL as is if URI cannot be parsed.
+ Log.LogWarning(CommonResources.RepositoryUrlIsNotComplete, scmRepoUrl);
+ setScmRepoUrlMetaData(scmRepoUrl);
+ }
+
+ void setScmRepoUrlMetaData(string url)
+ {
+ // Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata
+ // stores the value as specified. When initializing the URL metadata from git
+ // information we msbuild-escaped the URL to preserve any URL escapes in it.
+ // Here, GetMetadata unescapes the msbuild escapes, then we translate the URL
+ // and finally msbuild-escape the resulting URL to preserve any URL escapes.
+ sourceRoot.SetMetadata(Names.SourceRoot.ScmRepositoryUrl,
+ Evaluation.ProjectCollection.Escape(url));
+ }
}
}
}
diff --git a/src/Common/xlf/CommonResources.cs.xlf b/src/Common/xlf/CommonResources.cs.xlf
index f1ebb8b0..95269ffb 100644
--- a/src/Common/xlf/CommonResources.cs.xlf
+++ b/src/Common/xlf/CommonResources.cs.xlf
@@ -12,6 +12,16 @@
Položka {0} ve skupině položek {1} musí uvádět metadata {2}.
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ Nejde určit adresu URL úložiště. Zdrojový kód nebude k dispozici přes zdrojový odkaz.
diff --git a/src/Common/xlf/CommonResources.de.xlf b/src/Common/xlf/CommonResources.de.xlf
index e58ec006..7a9bc620 100644
--- a/src/Common/xlf/CommonResources.de.xlf
+++ b/src/Common/xlf/CommonResources.de.xlf
@@ -12,6 +12,16 @@
Das Element "{0}" der Elementgruppe "{1}" muss die Metadaten "{2}" angeben.
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ Die URL für das Repository konnte nicht ermittelt werden. Daher ist der Quellcode nicht über den Link zur Quelle verfügbar.
diff --git a/src/Common/xlf/CommonResources.es.xlf b/src/Common/xlf/CommonResources.es.xlf
index 6c77c859..2419fc7d 100644
--- a/src/Common/xlf/CommonResources.es.xlf
+++ b/src/Common/xlf/CommonResources.es.xlf
@@ -12,6 +12,16 @@
El elemento "{0}" del grupo de elementos "{1}" debe especificar los metadatos "{2}"
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ No se puede determinar la URL del repositorio, el código fuente no estará disponible a través del vínculo de origen.
diff --git a/src/Common/xlf/CommonResources.fr.xlf b/src/Common/xlf/CommonResources.fr.xlf
index ffb60259..7b541b50 100644
--- a/src/Common/xlf/CommonResources.fr.xlf
+++ b/src/Common/xlf/CommonResources.fr.xlf
@@ -4,42 +4,52 @@
- Le groupe d'éléments {0} est vide. Au moins un hôte de dépôt {1} est nécessaire pour générer SourceLink.
+ {0} item group is empty. At least one {1} repository host is required in order to generate SourceLink.
- L'élément '{0}' du groupe d'éléments '{1}' doit spécifier les métadonnées '{2}'
+ Item '{0}' of item group '{1}' must specify metadata '{2}'
+
+
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
- Impossible de déterminer l'URL du dépôt, le code source n'est pas disponible via le lien source.
+ Unable to determine repository url, the source code won't be available via source link.
- La valeur de {0} avec l'identité '{1}' n’est pas valide : '{2}'
+ The value of {0} with identity '{1}' is invalid: '{2}'
- La valeur de {0} avec l'identité '{1}' n'est pas un hachage de validation valide : '{2}'
+ The value of {0} with identity '{1}' is not a valid commit hash: '{2}'
- La valeur passée au paramètre de tâche {0} n'est pas un nom de domaine valide : '{1}'
+ The value passed to task parameter {0} is not a valid domain name: '{1}'
- La valeur passée au paramètre de tâche {0} n'est pas un URI d'hôte valide : '{1}'
+ The value passed to task parameter {0} is not a valid host URI: '{1}'
- La valeur passée au paramètre de tâche {0} n'est pas un URI valide : '{1}'
+ The value passed to task parameter {0} is not a valid URI: '{1}'
diff --git a/src/Common/xlf/CommonResources.it.xlf b/src/Common/xlf/CommonResources.it.xlf
index 222ef236..3531c844 100644
--- a/src/Common/xlf/CommonResources.it.xlf
+++ b/src/Common/xlf/CommonResources.it.xlf
@@ -12,6 +12,16 @@
L'elemento '{0}' del gruppo di elementi '{1}' deve specificare i metadati '{2}'
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ Non è possibile determinare l'URL del repository. Il codice sorgente non sarà disponibile tramite il collegamento all'origine.
diff --git a/src/Common/xlf/CommonResources.ja.xlf b/src/Common/xlf/CommonResources.ja.xlf
index 9457924d..69dc8e7b 100644
--- a/src/Common/xlf/CommonResources.ja.xlf
+++ b/src/Common/xlf/CommonResources.ja.xlf
@@ -12,6 +12,16 @@
項目グループ '{1}' の項目 '{0}' には、メタデータ '{2}' を指定する必要があります
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ リポジトリのURLを特定できません。ソース リンクを使用してソース コードを利用できません。
diff --git a/src/Common/xlf/CommonResources.ko.xlf b/src/Common/xlf/CommonResources.ko.xlf
index 178cd0af..2328edc7 100644
--- a/src/Common/xlf/CommonResources.ko.xlf
+++ b/src/Common/xlf/CommonResources.ko.xlf
@@ -12,6 +12,16 @@
항목 그룹 '{1}'의 '{0}' 항목은 '{2}' 메타데이터를 지정해야 합니다.
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ 리포지토리 URL을 확인할 수 없으며, 소스 링크를 통해 소스 코드를 사용할 수 없습니다.
diff --git a/src/Common/xlf/CommonResources.pl.xlf b/src/Common/xlf/CommonResources.pl.xlf
index e3021dcf..33550ba8 100644
--- a/src/Common/xlf/CommonResources.pl.xlf
+++ b/src/Common/xlf/CommonResources.pl.xlf
@@ -12,6 +12,16 @@
Element „{0}” grupy elementów „{1}” musi określać metadane „{2}”.
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ Nie można określić adresu URL repozytorium, kod źródłowy nie będzie dostępny za pośrednictwem linku do źródła.
diff --git a/src/Common/xlf/CommonResources.pt-BR.xlf b/src/Common/xlf/CommonResources.pt-BR.xlf
index d289ee2c..bb0d4e68 100644
--- a/src/Common/xlf/CommonResources.pt-BR.xlf
+++ b/src/Common/xlf/CommonResources.pt-BR.xlf
@@ -12,6 +12,16 @@
Item '{0}' do grupo de item '{1}' deve especificar metadados '{2}'
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ Não é possível determinar a URL do repositório, o código-fonte não estará disponível através do link de origem.
diff --git a/src/Common/xlf/CommonResources.ru.xlf b/src/Common/xlf/CommonResources.ru.xlf
index 290c9079..9856bab9 100644
--- a/src/Common/xlf/CommonResources.ru.xlf
+++ b/src/Common/xlf/CommonResources.ru.xlf
@@ -4,42 +4,52 @@
- Группа элементов {0} пуста. Для создания SourceLink нужно указать хотя бы один хост репозитория {1}.
+ {0} item group is empty. At least one {1} repository host is required in order to generate SourceLink.
- Необходимо указать метаданные '{2}' для элемента '{0}' в группе элементов '{1}'
+ Item '{0}' of item group '{1}' must specify metadata '{2}'
+
+
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
- Не удается определить URL-адрес репозитория, исходный код не будут доступен по ссылке.
+ Unable to determine repository url, the source code won't be available via source link.
- Значение {0} с идентификатором '{1}' является недопустимым: '{2}'
+ The value of {0} with identity '{1}' is invalid: '{2}'
- Значение {0} с идентификатором '{1}' не является допустимым хэшем фиксации: '{2}'
+ The value of {0} with identity '{1}' is not a valid commit hash: '{2}'
- Значение, передаваемое параметру задачи {0}, не является допустимым доменным именем: "{1}"
+ The value passed to task parameter {0} is not a valid domain name: '{1}'
- Значение, передаваемое параметру задачи {0}, не является допустимым URI узла: "{1}"
+ The value passed to task parameter {0} is not a valid host URI: '{1}'
- Значение, переданное в качестве параметра задачи {0}, не является допустимым URI: '{1}'
+ The value passed to task parameter {0} is not a valid URI: '{1}'
diff --git a/src/Common/xlf/CommonResources.tr.xlf b/src/Common/xlf/CommonResources.tr.xlf
index 89a36224..cf7c12f5 100644
--- a/src/Common/xlf/CommonResources.tr.xlf
+++ b/src/Common/xlf/CommonResources.tr.xlf
@@ -12,6 +12,16 @@
'{1}' öğe grubunun '{0}' öğesi, '{2}' meta verilerini belirtmelidir
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ Depo URL'si belirlenemiyor. Kaynak kod, kaynak bağlantısı aracılığıyla kullanılamayacak.
diff --git a/src/Common/xlf/CommonResources.zh-Hans.xlf b/src/Common/xlf/CommonResources.zh-Hans.xlf
index 74dc1582..19e12968 100644
--- a/src/Common/xlf/CommonResources.zh-Hans.xlf
+++ b/src/Common/xlf/CommonResources.zh-Hans.xlf
@@ -12,6 +12,16 @@
项组 "{1}" 的项 "{0}" 必须指定元数据 "{2}"
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ 无法确定存储库 URL,源代码将无法通过源链接获得。
diff --git a/src/Common/xlf/CommonResources.zh-Hant.xlf b/src/Common/xlf/CommonResources.zh-Hant.xlf
index 623c7110..03fadbf4 100644
--- a/src/Common/xlf/CommonResources.zh-Hant.xlf
+++ b/src/Common/xlf/CommonResources.zh-Hant.xlf
@@ -12,6 +12,16 @@
項目群組 '{1}' 的項目 '{0}' 必須指定中繼資料 '{2}'
+
+
+ The provided url '{0}' is not a complete URI
+
+
+
+
+ The {0} provider does not support translating the '{1}' protocol to content URLs.
+
+ 無法判斷存放庫 URL,無法透過原始連結取得原始程式碼。
diff --git a/src/SourceLink.Git.IntegrationTests/GitWebTests.cs b/src/SourceLink.Git.IntegrationTests/GitWebTests.cs
new file mode 100644
index 00000000..5b60d7cd
--- /dev/null
+++ b/src/SourceLink.Git.IntegrationTests/GitWebTests.cs
@@ -0,0 +1,130 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
+// License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using TestUtilities;
+
+namespace Microsoft.SourceLink.IntegrationTests
+{
+ public class GitWebTests : DotNetSdkTestBase
+ {
+ public GitWebTests()
+ : base("Microsoft.SourceLink.GitWeb")
+ {
+ }
+
+ [ConditionalFact(typeof(DotNetSdkAvailable))]
+ public void FullValidation_Ssh()
+ {
+ // Test non-ascii characters and escapes in the URL. Escaped URI reserved characters
+ // should remain escaped, non-reserved characters unescaped in the results.
+ var repoUrl = $"ssh://git@噸.com/test-%72epo\u1234%24%2572%2F.git";
+ var repoName = "test-repo\u1234%24%2572%2F.git";
+
+ var repo = GitUtilities.CreateGitRepositoryWithSingleCommit(ProjectDir.Path, new[] { ProjectFileName }, repoUrl);
+ var commitSha = repo.Head.Tip.Sha;
+
+ VerifyValues(
+ customProps: @"
+
+ true
+
+
+
+
+",
+ customTargets: "",
+ targets: new[]
+ {
+ "Build", "Pack"
+ },
+ expressions: new[]
+ {
+ "@(SourceRoot)",
+ "@(SourceRoot->'%(SourceLinkUrl)')",
+ "$(SourceLink)",
+ "$(PrivateRepositoryUrl)",
+ "$(RepositoryUrl)"
+ },
+ expectedResults: new[]
+ {
+ ProjectSourceRoot,
+ $"https://噸.com/gitweb/?p={repoName};a=blob_plain;hb={commitSha};f=*",
+ s_relativeSourceLinkJsonPath,
+ $"ssh://git@噸.com/{repoName}",
+ $"ssh://git@噸.com/{repoName}"
+ });
+
+ AssertEx.AreEqual(
+ $@"{{""documents"":{{""{ProjectSourceRoot.Replace(@"\", @"\\")}*"":""https://噸.com/gitweb/?p={repoName};a=blob_plain;hb={commitSha};f=*""}}}}",
+ File.ReadAllText(Path.Combine(ProjectDir.Path, s_relativeSourceLinkJsonPath)));
+
+ TestUtilities.ValidateAssemblyInformationalVersion(
+ Path.Combine(ProjectDir.Path, s_relativeOutputFilePath),
+ "1.0.0+" + commitSha);
+
+ TestUtilities.ValidateNuSpecRepository(
+ Path.Combine(ProjectDir.Path, s_relativePackagePath),
+ type: "git",
+ commit: commitSha,
+ url: $"ssh://git@噸.com/{repoName}");
+ }
+
+ [ConditionalFact(typeof(DotNetSdkAvailable))]
+ public void Issues_error_on_git_url()
+ {
+ var repoUrl = "git://噸.com/invalid_url_protocol.git";
+ var repo = GitUtilities.CreateGitRepositoryWithSingleCommit(ProjectDir.Path, new[] { ProjectFileName }, repoUrl);
+ var commitSha = repo.Head.Tip.Sha;
+
+ VerifyValues(
+ customProps: @"
+
+ true
+
+
+
+
+",
+ customTargets: "",
+ targets: new[]
+ {
+ "Build", "Pack"
+ },
+ expressions: Array.Empty(),
+ expectedErrors: new[]{
+ "The Microsoft.SourceLink.GitWeb provider does not support translating the 'git' protocol to content URLs."
+ });
+ }
+
+ [ConditionalFact(typeof(DotNetSdkAvailable))]
+ public void Issues_error_on_https_url()
+ {
+ var repoUrl = "https://噸.com/invalid_url_protocol.git";
+ var repo = GitUtilities.CreateGitRepositoryWithSingleCommit(ProjectDir.Path, new[] { ProjectFileName }, repoUrl);
+ var commitSha = repo.Head.Tip.Sha;
+
+ VerifyValues(
+ customProps: @"
+
+ true
+
+
+
+
+",
+ customTargets: "",
+ targets: new[]
+ {
+ "Build", "Pack"
+ },
+ expressions: Array.Empty(),
+ expectedErrors: new[]
+ {
+ // TODO how to reference resource?
+ "The Microsoft.SourceLink.GitWeb provider does not support translating the 'https' protocol to content URLs."
+ });
+ }
+ }
+}
diff --git a/src/SourceLink.GitWeb.UnitTests/GetSourceLinkUrlTests.cs b/src/SourceLink.GitWeb.UnitTests/GetSourceLinkUrlTests.cs
new file mode 100644
index 00000000..82da152f
--- /dev/null
+++ b/src/SourceLink.GitWeb.UnitTests/GetSourceLinkUrlTests.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
+// License.txt in the project root for license information.
+using Microsoft.Build.Tasks.SourceControl;
+using TestUtilities;
+using Xunit;
+using static TestUtilities.KeyValuePairUtils;
+
+namespace Microsoft.SourceLink.GitWeb.UnitTests
+{
+ public class GetSourceLinkUrlTests
+ {
+ [Fact]
+ public void EmptyHosts()
+ {
+ var engine = new MockEngine();
+
+ var task = new GetSourceLinkUrl()
+ {
+ BuildEngine = engine,
+ SourceRoot = new MockItem("x", KVP("RepositoryUrl", "http://abc.com"), KVP("SourceControl", "git")),
+ };
+
+ bool result = task.Execute();
+
+ AssertEx.AssertEqualToleratingWhitespaceDifferences(
+ "ERROR : " + string.Format(CommonResources.AtLeastOneRepositoryHostIsRequired, "SourceLinkGitWebHost", "GitWeb"), engine.Log);
+
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("", "/")]
+ [InlineData("/", "")]
+ [InlineData("/", "/")]
+ public void BuildSourceLinkUrl(string s1, string s2)
+ {
+ var engine = new MockEngine();
+
+ var task = new GetSourceLinkUrl()
+ {
+ BuildEngine = engine,
+ SourceRoot = new MockItem("/src/", KVP("RepositoryUrl", "ssh://git@src.intranet.company.com/root_dir_name/sub_dirs/reponame.git" + s1), KVP("SourceControl", "git"), KVP("RevisionId", "0123456789abcdefABCDEF000000000000000000")),
+ Hosts = new[]
+ {
+ // NOTE: i don't know what the spec parameter is for. but for all the other
+ // tests like this for various providers it is the domain of the Repo URL.
+ new MockItem("src.intranet.company.com", KVP("ContentUrl", "https://src.intranet.company.com/gitweb" + s2)),
+ }
+ };
+
+ bool result = task.Execute();
+ AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
+ AssertEx.AreEqual("https://src.intranet.company.com/gitweb/?p=root_dir_name/sub_dirs/reponame.git;a=blob_plain;hb=0123456789abcdefABCDEF000000000000000000;f=*", task.SourceLinkUrl);
+ Assert.True(result);
+ }
+ }
+}
diff --git a/src/SourceLink.GitWeb.UnitTests/Microsoft.SourceLink.GitWeb.UnitTests.csproj b/src/SourceLink.GitWeb.UnitTests/Microsoft.SourceLink.GitWeb.UnitTests.csproj
new file mode 100644
index 00000000..16608152
--- /dev/null
+++ b/src/SourceLink.GitWeb.UnitTests/Microsoft.SourceLink.GitWeb.UnitTests.csproj
@@ -0,0 +1,9 @@
+
+
+ net461;netcoreapp2.0
+
+
+
+
+
+
diff --git a/src/SourceLink.GitWeb.UnitTests/TranslateRepositoryUrlsTests.cs b/src/SourceLink.GitWeb.UnitTests/TranslateRepositoryUrlsTests.cs
new file mode 100644
index 00000000..255fdb2f
--- /dev/null
+++ b/src/SourceLink.GitWeb.UnitTests/TranslateRepositoryUrlsTests.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
+// License.txt in the project root for license information.
+using System.Linq;
+using TestUtilities;
+using Xunit;
+using static TestUtilities.KeyValuePairUtils;
+
+namespace Microsoft.SourceLink.GitWeb.UnitTests
+{
+ public class TranslateRepositoryUrlsTests
+ {
+ [Fact]
+ public void Translate()
+ {
+ var engine = new MockEngine();
+
+ var task = new TranslateRepositoryUrls()
+ {
+ BuildEngine = engine,
+ RepositoryUrl = "ssh://git@src.intranet.company.com/root_dir_name/sub_dirs/reponame.git",
+ IsSingleProvider = true,
+ SourceRoots = new[]
+ {
+ new MockItem("/1/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://git@src.intranet.company.com/root_dir_name/sub_dirs/reponame.git")),
+ new MockItem("/2/", KVP("SourceControl", "tfvc"), KVP("ScmRepositoryUrl", "ssh://git@src.intranet.company1.com/root_dir_name/sub_dirs/reponame.git")),
+ new MockItem("/2/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://git@src.intranet.company1.com/root_dir_name/sub_dirs/reponame.git")),
+ new MockItem("/2/", KVP("SourceControl", "tfvc"), KVP("ScmRepositoryUrl", "ssh://git@src.intranet.company2.com/root_dir_name/sub_dirs/reponame.git")),
+ },
+ Hosts = new[]
+ {
+ new MockItem("src.intranet.company1.com")
+ }
+ };
+
+ bool result = task.Execute();
+ AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log);
+
+ AssertEx.AreEqual("ssh://git@src.intranet.company.com/root_dir_name/sub_dirs/reponame.git", task.TranslatedRepositoryUrl);
+
+ AssertEx.Equal(new[]
+ {
+ "ssh://git@src.intranet.company.com/root_dir_name/sub_dirs/reponame.git",
+ "ssh://git@src.intranet.company1.com/root_dir_name/sub_dirs/reponame.git",
+ "ssh://git@src.intranet.company1.com/root_dir_name/sub_dirs/reponame.git",
+ "ssh://git@src.intranet.company2.com/root_dir_name/sub_dirs/reponame.git",
+ }, task.TranslatedSourceRoots.Select(r => r.GetMetadata("ScmRepositoryUrl")));
+
+ Assert.True(result);
+ }
+ }
+}
diff --git a/src/SourceLink.GitWeb/GetSourceLinkUrl.cs b/src/SourceLink.GitWeb/GetSourceLinkUrl.cs
new file mode 100644
index 00000000..da5df319
--- /dev/null
+++ b/src/SourceLink.GitWeb/GetSourceLinkUrl.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
+// License.txt in the project root for license information.
+
+using System;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Tasks.SourceControl;
+
+namespace Microsoft.SourceLink.GitWeb
+{
+ ///
+ /// The task calculates SourceLink URL for a given SourceRoot. If the SourceRoot is associated
+ /// with a git repository with a recognized domain the output
+ /// property is set to the content URL corresponding to the domain, otherwise it is set to
+ /// string "N/A".
+ ///
+ public sealed class GetSourceLinkUrl : GetSourceLinkUrlGitTask
+ {
+ protected override string HostsItemGroupName => "SourceLinkGitWebHost";
+ protected override string ProviderDisplayName => "GitWeb";
+
+ protected override Uri GetDefaultContentUriFromHostUri(string authority, Uri gitUri)
+ => new Uri($"https://{authority}/gitweb", UriKind.Absolute);
+
+ protected override string BuildSourceLinkUrl(Uri contentUri, Uri gitUri, string relativeUrl, string revisionId, ITaskItem? hostItem)
+ {
+ var trimLeadingSlash = relativeUrl.TrimStart('/');
+ var trimmedContentUrl = contentUri.ToString().TrimEnd('/', '\\');
+
+ // p = project/path
+ // a = action
+ // hb = SHA/revision
+ // f = repo file path
+ var gitwebRawUrl = UriUtilities.Combine(trimmedContentUrl, $"?p={trimLeadingSlash}.git;a=blob_plain;hb={revisionId};f=*");
+ return gitwebRawUrl;
+ }
+ }
+}
diff --git a/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.csproj b/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.csproj
new file mode 100644
index 00000000..fea0b608
--- /dev/null
+++ b/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.csproj
@@ -0,0 +1,33 @@
+
+
+ net461;netcoreapp2.0
+ true
+
+
+ true
+ $(MSBuildProjectName).nuspec
+ $(OutputPath)
+
+ Generates source link for Git repositories using a GitWeb server.
+ MSBuild Tasks GitWeb source link
+ true
+
+
+
+
+
+
+
+
+ Microsoft.Build.Tasks.SourceControl
+ true
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.nuspec b/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.nuspec
new file mode 100644
index 00000000..5acaea90
--- /dev/null
+++ b/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.nuspec
@@ -0,0 +1,17 @@
+
+
+
+ $CommonMetadataElements$
+
+
+
+
+
+
+ $CommonFileElements$
+
+
+
+
+
+
diff --git a/src/SourceLink.GitWeb/TranslateRepositoryUrls.cs b/src/SourceLink.GitWeb/TranslateRepositoryUrls.cs
new file mode 100644
index 00000000..69080d23
--- /dev/null
+++ b/src/SourceLink.GitWeb/TranslateRepositoryUrls.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See
+// License.txt in the project root for license information.
+
+using Microsoft.Build.Tasks.SourceControl;
+using System;
+
+namespace Microsoft.SourceLink.GitWeb
+{
+ ///
+ /// This task normally translates all protocols to HTTPS git repo URLs for consistency. Not all
+ /// GitWebs support HTTPS URLs for clones. So instead we keep the SSH URL. The
+ /// Task converts the URLs to HTTP content URLs which GitWeb does
+ /// support. The output of this Task is independent of .
+ ///
+ public sealed class TranslateRepositoryUrls : TranslateRepositoryUrlsGitTask
+ {
+ protected override bool IsProtocolSupportedByProvider(string protocol) => string.Equals(protocol, "ssh", StringComparison.Ordinal);
+
+ ///
+ /// Keep the SSH URL. It is the only protocol currently known to work with GitWeb.
+ ///
+ ///
+ /// as a string
+ protected override string TranslateSshUrl(Uri uri)
+ => uri.ToString();
+
+ protected override string TranslateGitUrl(Uri uri)
+ => throw new NotSupportedException("Git URLs are not supported by Microsoft.SourceLink.GitWeb.");
+
+ protected override string TranslateHttpUrl(Uri uri)
+ => throw new NotSupportedException("HTTP URLs are not supported by Microsoft.SourceLink.GitWeb.");
+ }
+}
diff --git a/src/SourceLink.GitWeb/build/Microsoft.SourceLink.GitWeb.targets b/src/SourceLink.GitWeb/build/Microsoft.SourceLink.GitWeb.targets
new file mode 100644
index 00000000..d5019e7a
--- /dev/null
+++ b/src/SourceLink.GitWeb/build/Microsoft.SourceLink.GitWeb.targets
@@ -0,0 +1,65 @@
+
+
+
+ <_SourceLinkGitWebAssemblyFile Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tools\net461\Microsoft.SourceLink.GitWeb.dll
+ <_SourceLinkGitWebAssemblyFile Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tools\netcoreapp2.0\Microsoft.SourceLink.GitWeb.dll
+
+
+
+
+
+
+ $(SourceLinkUrlInitializerTargets);_InitializeGitWebSourceLinkUrl
+ $(SourceControlManagerUrlTranslationTargets);TranslateGitWebUrlsInSourceControlInformation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_TranslatedSourceRoot Remove="@(_TranslatedSourceRoot)" />
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SourceLink.GitWeb/buildMultiTargeting/Microsoft.SourceLink.GitWeb.targets b/src/SourceLink.GitWeb/buildMultiTargeting/Microsoft.SourceLink.GitWeb.targets
new file mode 100644
index 00000000..84afbc37
--- /dev/null
+++ b/src/SourceLink.GitWeb/buildMultiTargeting/Microsoft.SourceLink.GitWeb.targets
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/SourceLink.GitWeb/xlf/Resources.cs.xlf b/src/SourceLink.GitWeb/xlf/Resources.cs.xlf
new file mode 100644
index 00000000..0e6540e3
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.cs.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Skupina položek {0} je prázdná. Kvůli generování zdrojového odkazu se vyžaduje alespoň jeden hostitel úložiště {1}.
+
+
+
+
+ {0} určuje neplatnou adresu URL: {1}.
+
+
+
+
+ Hodnota {0} s identitou {1} je neplatná: {2}"
+
+
+
+
+ Hodnota {0} s identitou {1} není platná hodnota hash potvrzení: {2}"
+
+
+
+
+ Hodnota předaná parametru úlohy {0} není platný název hostitele: {1}.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.de.xlf b/src/SourceLink.GitWeb/xlf/Resources.de.xlf
new file mode 100644
index 00000000..dcc9a38f
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.de.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Die Elementgruppe "{0}" ist leer. Mindestens ein {1}-Repositoryhost wird benötigt, um SourceLink zu generieren.
+
+
+
+
+ "{0}" gibt eine ungültige URL an: {1}
+
+
+
+
+ Der Wert von "{0}" mit der Identität "{1}" ist ungültig: "{2}"
+
+
+
+
+ Der Wert von "{0}" mit der Identität "{1}" ist kein gültiger Commithash: "{2}"
+
+
+
+
+ Der an den Taskparameter "{0}" übergebene Wert ist kein gültiger Hostname: "{1}"
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.es.xlf b/src/SourceLink.GitWeb/xlf/Resources.es.xlf
new file mode 100644
index 00000000..17b78f7c
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.es.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ El grupo de elementos {0} está vacío. Se requiere al menos un repositorio {1} a fin de generar SourceLink.
+
+
+
+
+ {0} especifica una dirección URL no válida: "{1}"
+
+
+
+
+ El valor de {0} con identidad '{1}' no es válido '{2}'"
+
+
+
+
+ El valor de {0} con identidad '{1}' no es un hash de confirmación válido '{2}'"
+
+
+
+
+ El valor pasado al parámetro de tarea {0} no es un nombre de host válido: "{1}"
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.fr.xlf b/src/SourceLink.GitWeb/xlf/Resources.fr.xlf
new file mode 100644
index 00000000..21751341
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.fr.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Le groupe d'éléments {0} est vide. Au moins un hôte de dépôt {1} est nécessaire pour générer SourceLink.
+
+
+
+
+ {0} spécifie une URL non valide : '{1}'
+
+
+
+
+ La valeur de {0} avec l'identité '{1}' n'est pas valide : '{2}'"
+
+
+
+
+ La valeur de {0} avec l'identité '{1}' n'est pas un hachage de validation valide : '{2}'"
+
+
+
+
+ La valeur passée au paramètre de tâche {0} n'est pas un nom d'hôte valide : '{1}'
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.it.xlf b/src/SourceLink.GitWeb/xlf/Resources.it.xlf
new file mode 100644
index 00000000..f6f3cb05
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.it.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Il gruppo di elementi {0} è vuoto. Per generare SourceLink, è necessario almeno un host di repository di {1}.
+
+
+
+
+ {0} specifica un URL non valido: '{1}'
+
+
+
+
+ Il valore di {0} con identità '{1}' non è valido: '{2}'"
+
+
+
+
+ Il valore di {0} con identità '{1}' non è un hash commit valido: '{2}'"
+
+
+
+
+ Il valore passato al parametro {0} dell'attività non è un nome host valido: '{1}'
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.ja.xlf b/src/SourceLink.GitWeb/xlf/Resources.ja.xlf
new file mode 100644
index 00000000..98145efd
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.ja.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ {0} 項目グループが空です。SourceLink を生成するには、少なくとも 1 つの {1} リポジトリ ホストが必要です。
+
+
+
+
+ {0} は無効な URL を指定しています: '{1}'
+
+
+
+
+ ID '{1}' の {0} の値は無効です: '{2}'"
+
+
+
+
+ ID '{1}' の {0} の値は有効なコミット ハッシュではありません: '{2}'"
+
+
+
+
+ タスク パラメーター {0} に渡された値は有効なホスト名ではありません: '{1}'
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.ko.xlf b/src/SourceLink.GitWeb/xlf/Resources.ko.xlf
new file mode 100644
index 00000000..ce5571ba
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.ko.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ {0} 항목 그룹이 비어 있습니다. SourceLink를 생성하려면 {1} 리포지토리 호스트가 하나 이상 필요합니다.
+
+
+
+
+ {0}이(가) 잘못된 URL을 지정합니다. '{1}'
+
+
+
+
+ ID가 '{1}'인 {0}의 값이 잘못되었습니다. '{2}'"
+
+
+
+
+ ID가 '{1}'인 {0}의 값이 유효한 커밋 해시가 아닙니다. '{2}'"
+
+
+
+
+ 작업 매개 변수 {0}에 전달되는 값이 유효한 호스트 이름이 아닙니다. '{1}'
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.pl.xlf b/src/SourceLink.GitWeb/xlf/Resources.pl.xlf
new file mode 100644
index 00000000..5f2e9268
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.pl.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Grupa elementów {0} jest pusta. Wymagany jest co najmniej jeden host repozytorium {1} w celu wygenerowania elementu SourceLink.
+
+
+
+
+ Element {0} określa nieprawidłowy adres URL: „{1}”
+
+
+
+
+ Wartość elementu {0} z tożsamością „{1}” jest nieprawidłowa: „{2}”"
+
+
+
+
+ Wartość elementu {0} z tożsamością „{1}” nie jest prawidłowym skrótem zatwierdzenia: „{2}”"
+
+
+
+
+ Wartość przekazana do parametru zadania {0} nie jest prawidłową nazwą hosta: „{1}”
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.pt-BR.xlf b/src/SourceLink.GitWeb/xlf/Resources.pt-BR.xlf
new file mode 100644
index 00000000..4a461578
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.pt-BR.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ O grupo de itens {0} está vazio. Pelo menos um host de repositório {1} é necessário para gerar o SourceLink.
+
+
+
+
+ {0} especifica uma URL inválida: '{1}'
+
+
+
+
+ O valor de {0} com a identidade '{1}' é inválido: '{2}'"
+
+
+
+
+ O valor de {0} com a identidade '{1}' não é um hash de confirmação válido: '{2}'"
+
+
+
+
+ O valor passado para o parâmetro de tarefa {0} não é um nome de host válido: '{1}'
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.ru.xlf b/src/SourceLink.GitWeb/xlf/Resources.ru.xlf
new file mode 100644
index 00000000..8683b5a7
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.ru.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Группа элементов {0} пуста. Для создания SourceLink нужно указать хотя бы один хост репозитория {1}.
+
+
+
+
+ В {0} указан недопустимый URL-адрес: "{1}"
+
+
+
+
+ Значение {0} с идентификатором "{1}" недопустимо: "{2}"
+
+
+
+
+ Значение {0} с идентификатором "{1}" не является допустимым хэш-кодом фиксации: "{2}"
+
+
+
+
+ Значение, передаваемое параметру задачи {0}, не является допустимым именем узла: "{1}"
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.tr.xlf b/src/SourceLink.GitWeb/xlf/Resources.tr.xlf
new file mode 100644
index 00000000..cb9305b8
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.tr.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ {0} öğe grubu boş. SourceLink oluşturmak için en az bir {1} depo konağı gereklidir.
+
+
+
+
+ {0} geçersiz bir URL belirtiyor: '{1}'
+
+
+
+
+ '{1}' kimliğine sahip {0} değeri geçersiz: '{2}'"
+
+
+
+
+ '{1}' kimliğine sahip {0} değeri geçerli bir işleme karması değil: '{2}'"
+
+
+
+
+ {0} görev parametresine geçirilen değer geçerli bir konak adı değil: '{1}'
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.zh-Hans.xlf b/src/SourceLink.GitWeb/xlf/Resources.zh-Hans.xlf
new file mode 100644
index 00000000..e4a4010d
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.zh-Hans.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ {0} 项目组为空。至少需要一个 {1} 存储库主机才能生成 SourceLink。
+
+
+
+
+ {0} 指定无效的 URL:“{1}”
+
+
+
+
+ 带有 '{1}' 标识的 {0} 的值无效: '{2}'"
+
+
+
+
+ 带有 '{1}' 标识的 {0} 的值不是有效的提交哈希: '{2}'"
+
+
+
+
+ 传递给任务参数 {0} 的值不是有效的主机名:“{1}”
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SourceLink.GitWeb/xlf/Resources.zh-Hant.xlf b/src/SourceLink.GitWeb/xlf/Resources.zh-Hant.xlf
new file mode 100644
index 00000000..ab6c29b8
--- /dev/null
+++ b/src/SourceLink.GitWeb/xlf/Resources.zh-Hant.xlf
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ {0} 項目群組是空的。至少需要一個 {1} 存放庫主機,才可產生 SourceLink。
+
+
+
+
+ {0} 指定的 URL 無效: '{1}'
+
+
+
+
+ 識別為 {1} 之 {0} 的值無效: '{2}'"
+
+
+
+
+ 識別為 {1} 之 {0} 的值不是有效的認可雜湊: '{2}'"
+
+
+
+
+ 傳遞給工作參數 {0} 的值並非有效的主機名稱: '{1}'
+
+
+
+
+
\ No newline at end of file