Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ The default value is `true`. Set to `false` to suppress publishing `SourceRevisi
Set by target `InitializeSourceControlInformationFromSourceControlManager` and consumed by NuGet `Pack` target and `GenerateAssemblyInfo` target.
May be used by custom targets that need this information.

### SourceBranchName

Set by target `InitializeSourceControlInformationFromSourceControlManager` and consumed by NuGet `Pack` target.
May be used by custom targets that need this information.

### EnableSourceLink

This property is implicitly set to `true` by a Source Link package. Including a Source Link package thus enables Source Link generation unless explicitly disabled by the project by setting this property to `false`.
Expand All @@ -165,6 +170,7 @@ Additional source-control specific metadata may be defined (depends on the sourc
For example, for Git:

- _RepositoryUrl_: e.g. `http://github.com/dotnet/corefx`
- _BranchName_: e.g. `refs/heads/main` (may be null if branch is in a detached HEAD state)

For TFVC:

Expand Down
1 change: 1 addition & 0 deletions src/Common/Utilities/Names.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal static class SourceRoot
public const string NestedRoot = nameof(NestedRoot);
public const string MappedPath = nameof(MappedPath);
public const string SourceLinkUrl = nameof(SourceLinkUrl);
public const string BranchName = nameof(BranchName);

public const string MappedPathFullName = nameof(SourceRoot) + "." + nameof(MappedPath);
public const string SourceLinkUrlFullName = nameof(SourceRoot) + "." + nameof(SourceLinkUrl);
Expand Down
24 changes: 22 additions & 2 deletions src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ private GitRepository CreateRepository(
string? workingDir = null,
GitConfig? config = null,
string? commitSha = null,
string? branchName = null,
ImmutableArray<GitSubmodule> submodules = default,
GitIgnore? ignore = null)
{
Expand All @@ -39,7 +40,8 @@ private GitRepository CreateRepository(
submodules.IsDefault ? ImmutableArray<GitSubmodule>.Empty : submodules,
submoduleDiagnostics: ImmutableArray<string>.Empty,
ignore ?? new GitIgnore(root: null, workingDir, ignoreCase: false),
commitSha);
commitSha,
branchName);
}

private GitSubmodule CreateSubmodule(string name, string relativePath, string url, string? headCommitSha, string? containingRepositoryWorkingDir = null)
Expand Down Expand Up @@ -316,6 +318,24 @@ public void GetSourceRoots_RepoWithoutCommits(bool warnOnMissingCommit)
AssertEx.Equal(warnOnMissingCommit ? new[] { Resources.RepositoryHasNoCommit } : Array.Empty<string>(), warnings.Select(TestUtilities.InspectDiagnostic));
}

[Fact]
public void GetSourceRoots_BranchName()
{
var repo = CreateRepository(
commitSha: "0000000000000000000000000000000000000000",
branchName: "refs/heads/abc");

var warnings = new List<(string, object?[])>();
var items = GitOperations.GetSourceRoots(repo, remoteName: null, warnOnMissingCommitOrUnsupportedUri: false, (message, args) => warnings.Add((message, args)));

AssertEx.Equal(new[]
{
$@"'{_workingDir}{s}' SourceControl='git' RevisionId='0000000000000000000000000000000000000000' BranchName='refs/heads/abc'",
}, items.Select(TestUtilities.InspectSourceRoot));

AssertEx.Equal(Array.Empty<string>(), warnings.Select(TestUtilities.InspectDiagnostic));
}

[Fact]
public void GetSourceRoots_RepoWithCommits_WithoutUrl()
{
Expand Down Expand Up @@ -468,7 +488,7 @@ public void GetSourceRoots_RelativeSubmodulePath(bool warnOnMissingCommitOrUnsup
Assert.Empty(warnings);
}
}

private static GitOperations.DirectoryNode CreateNode(string name, string? submoduleWorkingDirectory, List<GitOperations.DirectoryNode>? children = null)
=> new GitOperations.DirectoryNode(name, children ?? new List<GitOperations.DirectoryNode>())
{
Expand Down
32 changes: 32 additions & 0 deletions src/Microsoft.Build.Tasks.Git.UnitTests/GitRepositoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -539,5 +539,37 @@ public void GetSubmoduleHeadCommitSha_NoGitFile()

Assert.Null(GitRepository.GetSubmoduleReferenceResolver(submoduleWorkingDir.Path)?.ResolveHeadReference());
}

[Fact]
public void GetHeadBranchName()
{
using var temp = new TempRoot();

var commonDir = temp.CreateDirectory();
var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads");
refsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000 \t\v\r\n");

var gitDir = temp.CreateDirectory();
gitDir.CreateFile("HEAD").WriteAllText("ref: refs/heads/master \t\v\r\n");

var repository = new GitRepository(GitEnvironment.Empty, GitConfig.Empty, gitDir.Path, commonDir.Path, workingDirectory: null);
Assert.Equal("refs/heads/master", repository.GetBranchName());
}

[Fact]
public void GetHeadBranchName_DetachedHead()
{
using var temp = new TempRoot();

var commonDir = temp.CreateDirectory();
var refsHeadsDir = commonDir.CreateDirectory("refs").CreateDirectory("heads");
refsHeadsDir.CreateFile("master").WriteAllText("0000000000000000000000000000000000000000 \t\v\r\n");

var gitDir = temp.CreateDirectory();
gitDir.CreateFile("HEAD").WriteAllText("0000000000000000000000000000000000000000 \t\v\r\n");

var repository = new GitRepository(GitEnvironment.Empty, GitConfig.Empty, gitDir.Path, commonDir.Path, workingDirectory: null);
Assert.Null(repository.GetBranchName());
}
}
}
4 changes: 3 additions & 1 deletion src/Microsoft.Build.Tasks.Git.UnitTests/TestUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ public static string InspectSourceRoot(ITaskItem sourceRoot)
var containingRoot = sourceRoot.GetMetadata("ContainingRoot");
var scmRepositoryUrl = sourceRoot.GetMetadata("ScmRepositoryUrl");
var sourceLinkUrl = sourceRoot.GetMetadata("SourceLinkUrl");
var branchName = sourceRoot.GetMetadata("BranchName");

return $"'{sourceRoot.ItemSpec}'" +
(string.IsNullOrEmpty(sourceControl) ? "" : $" SourceControl='{sourceControl}'") +
(string.IsNullOrEmpty(revisionId) ? "" : $" RevisionId='{revisionId}'") +
(string.IsNullOrEmpty(nestedRoot) ? "" : $" NestedRoot='{nestedRoot}'") +
(string.IsNullOrEmpty(containingRoot) ? "" : $" ContainingRoot='{containingRoot}'") +
(string.IsNullOrEmpty(scmRepositoryUrl) ? "" : $" ScmRepositoryUrl='{scmRepositoryUrl}'") +
(string.IsNullOrEmpty(sourceLinkUrl) ? "" : $" SourceLinkUrl='{sourceLinkUrl}'");
(string.IsNullOrEmpty(sourceLinkUrl) ? "" : $" SourceLinkUrl='{sourceLinkUrl}'") +
(string.IsNullOrEmpty(branchName) ? "" : $" BranchName='{branchName}'");
}

public static string InspectDiagnostic((string Message, object?[] Args) warning)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;

Expand Down Expand Up @@ -152,23 +153,40 @@ internal static ImmutableDictionary<string, string> ReadPackedReferences(TextRea
public string? ResolveHeadReference()
=> ResolveReference(ReadReferenceFromFile(Path.Combine(_gitDirectory, GitRepository.GitHeadFileName)));

public string? GetBranchForHead()
{
string reference = ReadReferenceFromFile(Path.Combine(_gitDirectory, GitRepository.GitHeadFileName));

return TryGetReferenceName(reference, out var name) ? name : null;
}

public string? ResolveReference(string reference)
{
HashSet<string>? lazyVisitedReferences = null;
return ResolveReference(reference, ref lazyVisitedReferences);
}

private static bool TryGetReferenceName(string reference, [NotNullWhen(true)] out string? name)
{
const string refPrefix = "ref: ";
if (reference.StartsWith(refPrefix + RefsPrefix, StringComparison.Ordinal))
{
name = reference.Substring(refPrefix.Length);
return true;
}

name = null;
return false;
}

/// <exception cref="IOException"/>
/// <exception cref="InvalidDataException"/>
private string? ResolveReference(string reference, ref HashSet<string>? lazyVisitedReferences)
{
// See https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt-HEAD

const string refPrefix = "ref: ";
if (reference.StartsWith(refPrefix + RefsPrefix, StringComparison.Ordinal))
if (TryGetReferenceName(reference, out var symRef))
{
var symRef = reference.Substring(refPrefix.Length);

if (lazyVisitedReferences != null && !lazyVisitedReferences.Add(symRef))
{
// infinite recursion
Expand Down
12 changes: 11 additions & 1 deletion src/Microsoft.Build.Tasks.Git/GitDataReader/GitRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ internal sealed class GitRepository
private readonly Lazy<(ImmutableArray<GitSubmodule> Submodules, ImmutableArray<string> Diagnostics)> _lazySubmodules;
private readonly Lazy<GitIgnore> _lazyIgnore;
private readonly Lazy<string?> _lazyHeadCommitSha;
private readonly Lazy<string?> _lazyBranchName;
private readonly GitReferenceResolver _referenceResolver;

internal GitRepository(GitEnvironment environment, GitConfig config, string gitDirectory, string commonDirectory, string? workingDirectory)
Expand All @@ -71,6 +72,7 @@ internal GitRepository(GitEnvironment environment, GitConfig config, string gitD
_lazySubmodules = new Lazy<(ImmutableArray<GitSubmodule>, ImmutableArray<string>)>(ReadSubmodules);
_lazyIgnore = new Lazy<GitIgnore>(LoadIgnore);
_lazyHeadCommitSha = new Lazy<string?>(ReadHeadCommitSha);
_lazyBranchName = new Lazy<string?>(ReadBranchName);
}

// test only
Expand All @@ -83,12 +85,14 @@ internal GitRepository(
ImmutableArray<GitSubmodule> submodules,
ImmutableArray<string> submoduleDiagnostics,
GitIgnore ignore,
string? headCommitSha)
string? headCommitSha,
string? branchName)
: this(environment, config, gitDirectory, commonDirectory, workingDirectory)
{
_lazySubmodules = new Lazy<(ImmutableArray<GitSubmodule>, ImmutableArray<string>)>(() => (submodules, submoduleDiagnostics));
_lazyIgnore = new Lazy<GitIgnore>(() => ignore);
_lazyHeadCommitSha = new Lazy<string?>(() => headCommitSha);
_lazyBranchName = new Lazy<string?>(() => branchName);
}

/// <summary>
Expand Down Expand Up @@ -181,6 +185,12 @@ public static GitRepository OpenRepository(GitRepositoryLocation location, GitEn
private string? ReadHeadCommitSha()
=> _referenceResolver.ResolveHeadReference();

public string? GetBranchName()
=> _lazyBranchName.Value;

private string? ReadBranchName()
=> _referenceResolver.GetBranchForHead();

/// <summary>
/// Creates <see cref="GitReferenceResolver"/> for a submodule located in the specified <paramref name="submoduleWorkingDirectoryFullPath"/>.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Build.Tasks.Git/GitOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ public static ITaskItem[] GetSourceRoots(GitRepository repository, string? remot
{
// Don't report a warning since it has already been reported by GetRepositoryUrl task.
string? repositoryUrl = GetRepositoryUrl(repository, remoteName, logWarning: null);
string? branchName = repository.GetBranchName();

// Item metadata are stored msbuild-escaped. GetMetadata unescapes, SetMetadata stores the value as specified.
// Escape msbuild special characters so that URL escapes in the URL are preserved when the URL is read by GetMetadata.
Expand All @@ -259,6 +260,7 @@ public static ITaskItem[] GetSourceRoots(GitRepository repository, string? remot
item.SetMetadata(Names.SourceRoot.SourceControl, SourceControlName);
item.SetMetadata(Names.SourceRoot.ScmRepositoryUrl, Evaluation.ProjectCollection.Escape(repositoryUrl));
item.SetMetadata(Names.SourceRoot.RevisionId, revisionId);
item.SetMetadata(Names.SourceRoot.BranchName, Evaluation.ProjectCollection.Escape(branchName));
result.Add(item);
}
else if (warnOnMissingCommitOrUnsupportedUri)
Expand Down
7 changes: 7 additions & 0 deletions src/Microsoft.Build.Tasks.Git/LocateRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public sealed class LocateRepository : RepositoryTask
[Output]
public string? RevisionId { get; private set; }

/// <summary>
/// Branch name.
/// </summary>
[Output]
public string? BranchName { get; private set; }

protected override string? GetRepositoryId() => null;
protected override string GetInitialPath() => Path!;

Expand All @@ -56,6 +62,7 @@ private protected override void Execute(GitRepository repository)
Url = GitOperations.GetRepositoryUrl(repository, RemoteName, warnOnMissingOrUnsupportedRemote: !NoWarnOnMissingInfo, Log.LogWarning);
Roots = GitOperations.GetSourceRoots(repository, RemoteName, warnOnMissingCommitOrUnsupportedUri: !NoWarnOnMissingInfo, Log.LogWarning);
RevisionId = repository.GetHeadCommitSha();
BranchName = repository.GetBranchName();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
Reports a warning if the given project doesn't belong to a repository under source control,
unless the targets were implicily imported from an SDK without a package reference.
-->
<Microsoft.Build.Tasks.Git.LocateRepository
<Microsoft.Build.Tasks.Git.LocateRepository
Path="$(MSBuildProjectDirectory)"
RemoteName="$(GitRepositoryRemoteName)"
ConfigurationScope="$(GitRepositoryConfigurationScope)"
Expand All @@ -32,6 +32,7 @@
<Output TaskParameter="Url" PropertyName="ScmRepositoryUrl" />
<Output TaskParameter="Roots" ItemName="SourceRoot" />
<Output TaskParameter="RevisionId" PropertyName="SourceRevisionId" Condition="'$(SourceRevisionId)' == ''" />
<Output TaskParameter="BranchName" PropertyName="SourceBranchName" />
</Microsoft.Build.Tasks.Git.LocateRepository>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public void FullValidation_Https()
{
"@(SourceRoot)",
"@(SourceRoot->'%(SourceLinkUrl)')",
"@(SourceRoot->'%(BranchName)')",
"$(SourceLink)",
"$(PrivateRepositoryUrl)",
"$(RepositoryUrl)"
Expand All @@ -52,6 +53,7 @@ public void FullValidation_Https()
NuGetPackageFolders,
ProjectSourceRoot,
$"https://tfs.{TestStrings.DomainName}.local:8080/tfs/DefaultCollection/project/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*",
"refs/heads/main",
s_relativeSourceLinkJsonPath,
$"https://tfs.{TestStrings.DomainName}.local:8080/tfs/DefaultCollection/project/_git/{repoName}",
$"https://tfs.{TestStrings.DomainName}.local:8080/tfs/DefaultCollection/project/_git/{repoName}",
Expand Down Expand Up @@ -101,6 +103,7 @@ public void FullValidation_Ssh()
{
"@(SourceRoot)",
"@(SourceRoot->'%(SourceLinkUrl)')",
"@(SourceRoot->'%(BranchName)')",
"$(SourceLink)",
"$(PrivateRepositoryUrl)",
"$(RepositoryUrl)"
Expand All @@ -110,6 +113,7 @@ public void FullValidation_Ssh()
NuGetPackageFolders,
ProjectSourceRoot,
$"https://tfs.{TestStrings.DomainName}.local/tfs/DefaultCollection/project/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*",
"refs/heads/main",
s_relativeSourceLinkJsonPath,
$"https://tfs.{TestStrings.DomainName}.local/tfs/DefaultCollection/project/_git/{repoName}",
$"https://tfs.{TestStrings.DomainName}.local/tfs/DefaultCollection/project/_git/{repoName}",
Expand Down
4 changes: 4 additions & 0 deletions src/SourceLink.Git.IntegrationTests/AzureReposTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public void FullValidation_Https(string host)
{
"@(SourceRoot)",
"@(SourceRoot->'%(SourceLinkUrl)')",
"@(SourceRoot->'%(BranchName)')",
"$(SourceLink)",
"$(PrivateRepositoryUrl)",
"$(RepositoryUrl)"
Expand All @@ -52,6 +53,7 @@ public void FullValidation_Https(string host)
NuGetPackageFolders,
ProjectSourceRoot,
$"https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*",
"refs/heads/main",
s_relativeSourceLinkJsonPath,
$"https://test.{host}/test-org/_git/{repoName}",
$"https://test.{host}/test-org/_git/{repoName}",
Expand Down Expand Up @@ -100,6 +102,7 @@ public void FullValidation_Ssh(string host)
{
"@(SourceRoot)",
"@(SourceRoot->'%(SourceLinkUrl)')",
"@(SourceRoot->'%(BranchName)')",
"$(SourceLink)",
"$(PrivateRepositoryUrl)",
"$(RepositoryUrl)"
Expand All @@ -109,6 +112,7 @@ public void FullValidation_Ssh(string host)
NuGetPackageFolders,
ProjectSourceRoot,
$"https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*",
"refs/heads/main",
s_relativeSourceLinkJsonPath,
$"https://test.{host}/test-org/_git/{repoName}",
$"https://test.{host}/test-org/_git/{repoName}",
Expand Down
Loading