Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add git-based detection of tags at HEAD to improve PublicRelease detection #876

Merged
merged 30 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
317a142
Add wip tag lookup
georg-jung Dec 15, 2022
f0468bc
Add managed git support for annotated tags
georg-jung Dec 15, 2022
df58f36
Fix nullable warnings
georg-jung Dec 15, 2022
5f5b002
Fix mistaken comment in GitCommitReader
georg-jung Dec 15, 2022
dd9e383
Fix nullability issues and code style
georg-jung Dec 15, 2022
3c93a89
Add workaround for improper GitPack.TryGetValue function
georg-jung Dec 15, 2022
c2e3c84
Make HeadTags return null instead of throwing if no HEAD
georg-jung Dec 15, 2022
a5c61c4
Fix xml doc
georg-jung Dec 15, 2022
3b05482
Add some minor improvements
georg-jung Dec 15, 2022
9c7d4c5
Fix packed-refs handling
georg-jung Dec 15, 2022
066d82d
Make LibGit2Context behave like ManagedGit
georg-jung Dec 16, 2022
a3089b1
Add [Ignore] to exclude BuildingTags from CloudBuildAllVars
georg-jung Jan 4, 2023
46b3681
Cache tags collections
AArnott Feb 17, 2023
c321ea2
Reduce allocations for `GitAnnotatedTag` equality checks
AArnott Feb 17, 2023
a84ad8b
Skip tag candidate collection, reduce string operations
georg-jung Feb 17, 2023
49b9dc9
Fix handling of peel lines in packed-refs
georg-jung Feb 17, 2023
82c1751
Fix EnumeratePackedRefsWithPeelLines
georg-jung Feb 17, 2023
2198efa
Clarify EnumeratePackedRefsWithPeelLines
georg-jung Feb 17, 2023
41652e2
Speedup: Use packed-refs header, consider records peeled if applicable
georg-jung Feb 22, 2023
be0182b
Fix: read from disposed StreamReader
georg-jung Feb 22, 2023
89f29b1
Avoid exploring tags unless version.json needs it
AArnott Mar 7, 2023
a39f728
Merge remote-tracking branch 'origin/main' into georg-jung/main
AArnott Mar 17, 2023
cc2111b
Add first Tags test
AArnott Mar 17, 2023
3373263
Add annotated tag test
AArnott Mar 17, 2023
cd36215
Capture failure drop in its entirety
AArnott Mar 17, 2023
544704a
Add runtime check on ParseHex argument
AArnott Mar 17, 2023
fe34eb2
Collect .git directory too
AArnott Mar 20, 2023
9f52d81
Merge remote-tracking branch 'upstream/main'
georg-jung Sep 15, 2023
f0008fe
Adapt to changes on main
georg-jung Sep 15, 2023
60462f2
Touch-ups
AArnott Sep 15, 2023
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
7 changes: 7 additions & 0 deletions azure-pipelines/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ steps:
displayName: 🛠️ Build nerdbank-gitversioning NPM package
workingDirectory: src/nerdbank-gitversioning.npm

- pwsh: |
md $(Build.ArtifactStagingDirectory)\drop
7z a $(Build.ArtifactStagingDirectory)\drop\nbgv.7z * -r
Write-Host "##vso[artifact.upload containerfolder=drop;artifactname=drop;]$(Build.ArtifactStagingDirectory)\drop"
displayName: Capture .git directory
condition: and(failed(), eq(variables['Agent.OS'], 'Windows_NT'))

- powershell: azure-pipelines/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults
displayName: 🧪 dotnet test
condition: and(succeeded(), ${{ parameters.RunTests }})
Expand Down
2 changes: 2 additions & 0 deletions src/NerdBank.GitVersioning/DisabledGit/DisabledGitContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public DisabledGitContext(string workingTreePath)

public override string? HeadCanonicalName => null;

public override IReadOnlyCollection<string>? HeadTags => null;

private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (disabled-git)";

public override void ApplyTag(string name) => throw new NotSupportedException();
Expand Down
5 changes: 5 additions & 0 deletions src/NerdBank.GitVersioning/GitContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ public string RepoRelativeProjectDirectory
/// </summary>
public abstract string? HeadCanonicalName { get; }

/// <summary>
/// Gets a collection of the tags that reference HEAD.
/// </summary>
public abstract IReadOnlyCollection<string>? HeadTags { get; }

/// <summary>
/// Gets the path to the .git folder.
/// </summary>
Expand Down
19 changes: 19 additions & 0 deletions src/NerdBank.GitVersioning/LibGit2/LibGit2Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ namespace Nerdbank.GitVersioning.LibGit2;
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
public class LibGit2Context : GitContext
{
/// <summary>
/// Caching field behind <see cref="HeadTags" /> property.
/// </summary>
private IReadOnlyCollection<string>? headTags;

internal LibGit2Context(string workingTreeDirectory, string dotGitPath, string? committish = null)
: base(workingTreeDirectory, dotGitPath)
{
Expand Down Expand Up @@ -51,6 +56,20 @@ internal LibGit2Context(string workingTreeDirectory, string dotGitPath, string?
/// <inheritdoc />
public override string HeadCanonicalName => this.Repository.Head.CanonicalName;

/// <inheritdoc />
public override IReadOnlyCollection<string>? HeadTags
{
get
{
return this.headTags ??= this.Commit is not null
? this.Repository.Tags
.Where(tag => tag.Target.Sha.Equals(this.Commit.Sha))
.Select(tag => tag.CanonicalName)
.ToList()
: null;
}
}

private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (libgit2)";

/// <summary>Initializes a new instance of the <see cref="LibGit2Context"/> class.</summary>
Expand Down
11 changes: 11 additions & 0 deletions src/NerdBank.GitVersioning/Managed/ManagedGitContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ namespace Nerdbank.GitVersioning.Managed;
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
public class ManagedGitContext : GitContext
{
/// <summary>
/// Caching field behind <see cref="HeadTags" /> property.
/// </summary>
private IReadOnlyCollection<string>? headTags;

internal ManagedGitContext(string workingDirectory, string dotGitPath, string? committish = null)
: base(workingDirectory, dotGitPath)
{
Expand Down Expand Up @@ -53,6 +58,12 @@ internal ManagedGitContext(string workingDirectory, string dotGitPath, string? c
/// <inheritdoc />
public override string HeadCanonicalName => this.Repository.GetHeadAsReferenceOrSha().ToString() ?? throw new InvalidOperationException("Unable to determine the HEAD position.");

/// <inheritdoc />
public override IReadOnlyCollection<string>? HeadTags
{
get => this.headTags ??= this.Repository.Lookup("HEAD") is GitObjectId head ? this.Repository.LookupTags(head) : null;
}

private string DebuggerDisplay => $"\"{this.WorkingTreePath}\" (managed)";

/// <summary>Initializes a new instance of the <see cref="ManagedGitContext"/> class.</summary>
Expand Down
53 changes: 53 additions & 0 deletions src/NerdBank.GitVersioning/ManagedGit/GitAnnotatedTag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#nullable enable

using System.Collections;

namespace Nerdbank.GitVersioning.ManagedGit;

/// <summary>
/// Represents a Git annotated tag, as stored in the Git object database.
/// </summary>
public struct GitAnnotatedTag : IEquatable<GitAnnotatedTag>
{
/// <summary>
/// Gets or sets the <see cref="GitObjectId"/> of object this tag is pointing at.
/// </summary>
public GitObjectId Object { get; set; }

/// <summary>
/// Gets or sets a <see cref="GitObjectId"/> which uniquely identifies the <see cref="GitAnnotatedTag"/>.
/// </summary>
public GitObjectId Sha { get; set; }

/// <summary>
/// Gets or sets the tag name of this annotated tag.
/// </summary>
public string Tag { get; set; }

/// <summary>
/// Gets or sets the type of the object this tag is pointing to, e.g. "commit" or, for nested tags, "tag".
/// </summary>
public string Type { get; set; }

public static bool operator ==(GitAnnotatedTag left, GitAnnotatedTag right) => left.Equals(right);

public static bool operator !=(GitAnnotatedTag left, GitAnnotatedTag right) => !(left == right);

/// <inheritdoc/>
public override bool Equals(object? obj) => obj is GitAnnotatedTag tag ? this.Equals(tag) : false;

/// <inheritdoc/>
public bool Equals(GitAnnotatedTag other) => this.Sha.Equals(other.Sha);

/// <inheritdoc/>
public override int GetHashCode() => this.Sha.GetHashCode();

/// <inheritdoc/>
public override string ToString()
{
return $"Git Tag: {this.Tag} with id {this.Sha}";
}
}
133 changes: 133 additions & 0 deletions src/NerdBank.GitVersioning/ManagedGit/GitAnnotatedTagReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#nullable enable

using System.Buffers;
using System.Diagnostics;
using System.Xml.Linq;
using LibGit2Sharp;
using Validation;

namespace Nerdbank.GitVersioning.ManagedGit;

/// <summary>
/// Reads a <see cref="GitAnnotatedTag"/> object.
/// </summary>
public static class GitAnnotatedTagReader
{
private const int ObjectLineLength = 48;

private static readonly byte[] ObjectStart = GitRepository.Encoding.GetBytes("object ");
private static readonly byte[] TypeStart = GitRepository.Encoding.GetBytes("type ");
private static readonly byte[] TagStart = GitRepository.Encoding.GetBytes("tag ");

/// <summary>
/// Reads a <see cref="GitAnnotatedTag"/> object from a <see cref="Stream"/>.
/// </summary>
/// <param name="stream">
/// A <see cref="Stream"/> which contains the <see cref="GitAnnotatedTag"/> in its text representation.
/// </param>
/// <param name="sha">
/// The <see cref="GitObjectId"/> of the commit.
/// </param>
/// <returns>
/// The <see cref="GitAnnotatedTag"/>.
/// </returns>
public static GitAnnotatedTag Read(Stream stream, GitObjectId sha)
{
Requires.NotNull(stream, nameof(stream));

byte[] buffer = ArrayPool<byte>.Shared.Rent(checked((int)stream.Length));

try
{
Span<byte> span = buffer.AsSpan(0, (int)stream.Length);
stream.ReadAll(span);

return Read(span, sha);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

/// <summary>
/// Reads a <see cref="GitAnnotatedTag"/> object from a <see cref="ReadOnlySpan{T}"/>.
/// </summary>
/// <param name="tag">
/// A <see cref="ReadOnlySpan{T}"/> which contains the <see cref="GitAnnotatedTag"/> in its text representation.
/// </param>
/// <param name="sha">
/// The <see cref="GitObjectId"/> of the annotated tag.
/// </param>
/// <returns>
/// The <see cref="GitAnnotatedTag"/>.
/// </returns>
public static GitAnnotatedTag Read(ReadOnlySpan<byte> tag, GitObjectId sha)
{
ReadOnlySpan<byte> buffer = tag;

GitObjectId obj = ReadObject(buffer.Slice(0, ObjectLineLength));
buffer = buffer.Slice(ObjectLineLength);

(string type, int typeLen) = ReadType(buffer);
buffer = buffer.Slice(typeLen);

(string tagName, _) = ReadTag(buffer);

return new GitAnnotatedTag
{
Sha = sha,
Object = obj,
Type = type,
Tag = tagName,
};
}

private static GitObjectId ReadObject(ReadOnlySpan<byte> line)
{
// Format: object d8329fc1cc938780ffdd9f94e0d364e0ea74f579\n
// 48 bytes:
// object: 6 bytes
// space: 1 byte
// hash: 40 bytes
// \n: 1 byte
Debug.Assert(line.Slice(0, ObjectStart.Length).SequenceEqual(ObjectStart));
AArnott marked this conversation as resolved.
Show resolved Hide resolved
Debug.Assert(line[ObjectLineLength - 1] == (byte)'\n');

return GitObjectId.ParseHex(line.Slice(ObjectStart.Length, 40));
}

private static (string Content, int BytesRead) ReadPrefixedString(ReadOnlySpan<byte> remaining, byte[] prefix)
{
Debug.Assert(remaining.Slice(0, prefix.Length).SequenceEqual(prefix));

int lineEnd = remaining.IndexOf((byte)'\n');
ReadOnlySpan<byte> type = remaining.Slice(prefix.Length, lineEnd - prefix.Length);
return (GitRepository.GetString(type), lineEnd + 1);
}

private static (string Content, int BytesRead) ReadType(ReadOnlySpan<byte> remaining)
{
// Format: type commit\n
// <variable> bytes:
// type: 4 bytes
// space: 1 byte
// <type e.g. commit>: <variable> bytes
// \n: 1 byte
return ReadPrefixedString(remaining, TypeStart);
}

private static (string Content, int BytesRead) ReadTag(ReadOnlySpan<byte> remaining)
{
// Format: tag someAnnotatedTag\n
// <variable> bytes:
// tag: 3 bytes
// space: 1 byte
// <tag name e.g. someAnnotatedTag>: <variable> bytes
// \n: 1 byte
return ReadPrefixedString(remaining, TagStart);
}
}
4 changes: 2 additions & 2 deletions src/NerdBank.GitVersioning/ManagedGit/GitCommitReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ public static GitCommit Read(ReadOnlySpan<byte> commit, GitObjectId sha, bool re
private static GitObjectId ReadTree(ReadOnlySpan<byte> line)
{
// Format: tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579\n
// 47 bytes:
// tree: 5 bytes
// 46 bytes:
// tree: 4 bytes
// space: 1 byte
// hash: 40 bytes
// \n: 1 byte
Expand Down
32 changes: 26 additions & 6 deletions src/NerdBank.GitVersioning/ManagedGit/GitObjectId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,17 @@ public static GitObjectId Parse(ReadOnlySpan<byte> value)
}

/// <summary>
/// Parses a <see cref="string"/> which contains the hexadecimal representation of a
/// <see cref="GitObjectId"/>.
/// Parses a <see cref="ReadOnlySpan{T}"/> of <see cref="char"/> which contains the
/// hexadecimal representation of a <see cref="GitObjectId"/>.
/// </summary>
/// <param name="value">
/// A <see cref="string"/> which contains the hexadecimal representation of the
/// <see cref="GitObjectId"/>.
/// A <see cref="ReadOnlySpan{T}"/> of <see cref="char"/> which contains the
/// hexadecimal representation of the <see cref="GitObjectId"/>.
/// </param>
/// <returns>
/// A <see cref="GitObjectId"/>.
/// </returns>
public static GitObjectId Parse(string value)
public static GitObjectId Parse(ReadOnlySpan<char> value)
{
Debug.Assert(value.Length == 40);

Expand All @@ -92,6 +92,23 @@ public static GitObjectId Parse(string value)
bytes[i >> 1] = (byte)(c1 + c2);
}

return objectId;
}

/// <summary>
/// Parses a <see cref="string"/> which contains the hexadecimal representation of a
/// <see cref="GitObjectId"/>.
/// </summary>
/// <param name="value">
/// A <see cref="string"/> which contains the hexadecimal representation of the
/// <see cref="GitObjectId"/>.
/// </param>
/// <returns>
/// A <see cref="GitObjectId"/>.
/// </returns>
public static GitObjectId Parse(string value)
{
GitObjectId objectId = Parse(value.AsSpan());
objectId.sha = value.ToLower();
return objectId;
}
Expand All @@ -109,7 +126,10 @@ public static GitObjectId Parse(string value)
/// </returns>
public static GitObjectId ParseHex(ReadOnlySpan<byte> value)
{
Debug.Assert(value.Length == 40);
if (value.Length != 40)
{
throw new ArgumentException($"Length should be exactly 40, but was {value.Length}.", nameof(value));
}

var objectId = default(GitObjectId);
Span<byte> bytes = objectId.Value;
Expand Down
21 changes: 18 additions & 3 deletions src/NerdBank.GitVersioning/ManagedGit/GitPack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable enable

using System.Diagnostics.CodeAnalysis;
using System.IO.MemoryMappedFiles;
using System.Text;

Expand Down Expand Up @@ -141,7 +142,7 @@ public GitPack(GetObjectFromRepositoryDelegate getObjectFromRepositoryDelegate,
/// <returns>
/// <see langword="true"/> if the object was found; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetObject(GitObjectId objectId, string objectType, out Stream? value)
public bool TryGetObject(GitObjectId objectId, string objectType, [NotNullWhen(true)] out Stream? value)
{
long? offset = this.GetOffset(objectId);

Expand All @@ -152,8 +153,18 @@ public bool TryGetObject(GitObjectId objectId, string objectType, out Stream? va
}
else
{
value = this.GetObject(offset.Value, objectType);
return true;
// This try-catch should probably be replaced by a non-throwing GetObject implementation.
// This is in turn dependend on a proper GitPackReader.TryGetObject implementation.
try
{
value = this.GetObject(offset.Value, objectType);
return true;
}
catch (GitException gexc) when (gexc.ErrorCode == GitException.ErrorCodes.ObjectNotFound)
{
value = null;
return false;
}
}
}

Expand Down Expand Up @@ -204,6 +215,10 @@ public Stream GetObject(long offset, string objectType)
packObjectType = GitPackObjectType.OBJ_BLOB;
break;

case "tag":
packObjectType = GitPackObjectType.OBJ_TAG;
break;

default:
throw new GitException($"The object type '{objectType}' is not supported by the {nameof(GitPack)} class.");
}
Expand Down
Loading