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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ The patch number restarted from 1, below the 2.0.x versions already published.
- `bv release` runs the [build pipeline](docs/tool-commands/release.md#the-steps) once, from `clean` through `pack`, after the release commit, and no longer runs `clean` through `test` before it.
- [The pages under `docs/`](docs/README.md) moved to kebab-case names, `docs/modules/` to `docs/sdk-modules/`, and `docs/DependencyManagement.md` to `docs/tool-commands/dependencies.md`, so a link to an old name breaks.
- The `StandardAnalyzers` module adds [`StyleCop.Analyzers` 1.3.133-beta](docs/sdk-modules/standard-analyzers.md#package-references), which supports C# 8 to 15 and adds several new rules.
- `bv release` reaches the [GitHub API of the host of the repository](docs/tool-commands/release.md#publishing), `api.github.com` or `<host>/api/v3`, where it reached `api.github.com` alone.

### Bugs fixed in this release

Expand Down
1 change: 0 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
<PackageVersion Include="NuGet.ProjectModel" Version="7.9.0" />
<PackageVersion Include="NuGet.Protocol" Version="7.9.0" />
<PackageVersion Include="NuGet.Versioning" Version="7.9.0" />
<PackageVersion Include="Octokit" Version="14.0.0" />
<PackageVersion Include="Spectre.Console" Version="0.57.2" />
</ItemGroup>

Expand Down
5 changes: 5 additions & 0 deletions docs/tool-commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ The release assets are collected from two sources:

A line with the wrong shape, or one naming a file that does not exist, is a warning, and the asset is skipped.

`bv` reaches the GitHub API of the host of the `origin` remote.
For `github.com`, the API is at `https://api.github.com`.
`bv` takes any other host for a GitHub Enterprise Server, whose API is at `https://<host>/api/v3`.
An asset upload goes to the address that GitHub states in the answer to the creation of the draft release.

`bv` then uploads the assets, and turns the draft into the release.
The release is named after the version, its tag names the release commit, and the prerelease flag follows the version.
Its description is a link to `CHANGELOG.md` on the release branch, followed by the notes GitHub generates.
Expand Down
1 change: 0 additions & 1 deletion src/Buildvana.Tool/Buildvana.Tool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
<PackageReference Include="NuGet.ProjectModel" />
<PackageReference Include="NuGet.Protocol" />
<PackageReference Include="NuGet.Versioning" />
<PackageReference Include="Octokit" />
<PackageReference Include="Spectre.Console" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public IServiceCollection AddBvServices()
.AddSingleton<IProcessRunner, ProcessRunner>()
.AddSingleton<ISolutionContextFactory, HomeDirectorySolutionContextFactory>()
.AddSingleton<SolutionContext>(static sp => sp.GetRequiredService<ISolutionContextFactory>().Create())
.AddSingleton<GitService>()
.AddSingleton<IGitService, GitService>()
.AddSingleton<DeclaredApiFilesService>()
.AddSingleton(ServerAdapter.Create)
.AddSingleton<VersioningSettings>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,15 @@ private static bool IsAtOrAbove(string channelVersion, NuGetVersion pinnedVersio

private async Task<JsonDocument> ReadJsonAsync(Uri url, CancellationToken cancellationToken)
{
JsonDocument document;
try
{
using var response = await _http.GetAsync(url, cancellationToken).ConfigureAwait(false);
_ = response.EnsureSuccessStatusCode();
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
return await JsonDocument.ParseAsync(stream, default, cancellationToken).ConfigureAwait(false);
document = await JsonDocument.ParseAsync(stream, default, cancellationToken).ConfigureAwait(false);
}
}
catch (HttpRequestException exception)
Expand All @@ -204,5 +205,7 @@ private async Task<JsonDocument> ReadJsonAsync(Uri url, CancellationToken cancel
{
throw new BuildFailedException($"The .NET release index at {url} states something bv cannot read: {exception.Message}", exception);
}

return document;
}
}
96 changes: 16 additions & 80 deletions src/Buildvana.Tool/Services/Git/GitService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
namespace Buildvana.Tool.Services.Git;

/// <summary>
/// Provides shortcut methods to use Git.
/// Implements <see cref="IGitService"/> over LibGit2Sharp.
/// </summary>
[PublicAPI]
internal sealed class GitService : IDisposable
internal sealed class GitService : IGitService, IDisposable
{
private readonly IReporter _reporter;
private readonly IHomeDirectoryProvider _home;
Expand All @@ -47,25 +47,16 @@ public GitService(IReporter reporter, IHomeDirectoryProvider home)
CurrentBranch = headName.StartsWith("refs/heads/", StringComparison.Ordinal) ? _repository.Head.FriendlyName : string.Empty;
}

/// <summary>
/// Gets the name of the origin remote, i.e. either "origin" if such remote exists, or the name of the only remote if there is only one.
/// </summary>
/// <inheritdoc/>
public string Origin { get; }

/// <summary>
/// Gets the fetch URL of the origin remote.
/// </summary>
/// <inheritdoc/>
public Uri OriginUrl { get; }

/// <summary>
/// Gets the name of the current Git branch.
/// </summary>
/// <value>If HEAD is on a branch, the name of the branch; otherwise, the empty string.</value>
/// <inheritdoc/>
public string CurrentBranch { get; }

/// <summary>
/// Gets or sets the identity of the Git committer.
/// </summary>
/// <inheritdoc/>
[DisallowNull]
public GitIdentity? CommitterIdentity
{
Expand All @@ -84,45 +75,23 @@ public GitIdentity? CommitterIdentity
}
}

/// <summary>
/// Gets or sets the credentials used for pushing to the Git repository if ambient credentials are not sufficient.
/// </summary>
/// <remarks>
/// <para>Set this property when ambient mechanisms (`http.extraheader` written by CI checkout actions,
/// URL-embedded credentials, OS credential helpers) are absent or insufficient.</para>
/// <para>The provided credentials are tried only after the server returns a 401 challenge to the initial push request.</para>
/// </remarks>
/// <inheritdoc/>
public GitCredentials? PushCredentialsFallback { get; set; }

/// <summary>
/// Gets the SHA of the current <c>HEAD</c> commit.
/// </summary>
/// <inheritdoc/>
public string HeadSha => _repository.Head.Tip.Sha;

/// <inheritdoc cref="IDisposable.Dispose"/>
public void Dispose() => _repository.Dispose();

/// <summary>
/// Tells whether a tag exists in the local Git repository.
/// </summary>
/// <param name="tag">The tag to check for.</param>
/// <returns>If a tag whose name is equal to <paramref name="tag"/> exists in the repository, <see langword="true"/>;
/// otherwise, <see langword="false"/>.</returns>
/// <inheritdoc/>
public bool TagExists(string tag)
{
Guard.IsNotNullOrEmpty(tag);
return _repository.Tags.Any(x => string.Equals(x.FriendlyName, tag, StringComparison.Ordinal));
}

/// <summary>
/// Gets the latest version and the latest stable version in commit history.
/// </summary>
/// <returns>A tuple of the latest version and the latest stable version.</returns>
/// <remarks>
/// <para>If no version tag is found in commit history, this method returns a tuple of two <see langword="null"/>s.</para>
/// <para>If no stable version tag is found in commit history, this method returns a tuple of the latest
/// version and <see langword="null"/>.</para>
/// </remarks>
/// <inheritdoc/>
public (SemanticVersion? Latest, SemanticVersion? LatestStable) GetLatestVersions()
{
var versions = _repository.Tags
Expand Down Expand Up @@ -154,15 +123,7 @@ public bool TagExists(string tag)
return (latest, latestStable);
}

/// <summary>
/// Runs an operation and detects the files it modified, by comparing working-tree snapshots
/// (as per <see cref="GetDirtyFiles"/>) taken before and after the operation.
/// </summary>
/// <typeparam name="T">The type of the operation's result.</typeparam>
/// <param name="operation">The operation to run.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the ongoing operation, whose result is a tuple
/// of the operation's result and the absolute paths of the files the operation modified. Files that
/// were already dirty before the operation are not reported, even if the operation modified them again.</returns>
/// <inheritdoc/>
public async Task<(T Result, IReadOnlyList<string> ChangedFiles)> TrackChangesAsync<T>(Func<Task<T>> operation)
{
Guard.IsNotNull(operation);
Expand All @@ -172,13 +133,7 @@ public bool TagExists(string tag)
return (result, changedFiles);
}

/// <summary>
/// Gets the files that are new, modified, or otherwise different between the working directory
/// (including the index) and the current <c>HEAD</c> commit. Ignored files are not considered;
/// neither are files under bv's scratch directory (<see cref="CommonPaths.Scratch"/>),
/// whether or not the repository ignores it.
/// </summary>
/// <returns>The absolute paths of the differing files.</returns>
/// <inheritdoc/>
public IReadOnlyList<string> GetDirtyFiles()
{
var homeDirectory = _home.HomeDirectory;
Expand All @@ -195,10 +150,7 @@ .. _repository.RetrieveStatus(options)
];
}

/// <summary>
/// Adds one or more files to the Git index.
/// </summary>
/// <param name="paths">The paths of the files to add.</param>
/// <inheritdoc/>
public void Stage(params string[] paths)
{
Guard.IsNotNull(paths);
Expand Down Expand Up @@ -233,12 +185,7 @@ public void Stage(params string[] paths)
});
}

/// <summary>
/// Commits staged changes, or amends last commit.
/// </summary>
/// <param name="message">The commit message.</param>
/// <param name="amend">If <see langword="true"/>, amends last commit instead of creating a new commit.</param>
/// <param name="allowEmpty">If <see langword="true"/>, allows creating (or amending into) an empty commit.</param>
/// <inheritdoc/>
public void Commit(string message, bool amend = false, bool allowEmpty = false)
{
var signature = _repository.Config.BuildSignature(DateTimeOffset.Now);
Expand All @@ -247,16 +194,7 @@ public void Commit(string message, bool amend = false, bool allowEmpty = false)
_ = _repository.Commit(message, signature, signature, options);
}

/// <summary>
/// Undoes the most recent commit.
/// </summary>
/// <remarks>
/// <para>This method's purpose is to undo a commit that was just generated by code and is not a merge commit.</para>
/// <para>If the current <c>HEAD</c> has multiple parents, the behavior of this method is undefined.</para>
/// <para>If the repository has no commits, or the current <c>HEAD</c> has no parents, this method will fail.</para>
/// <para>Undoing a commit is not an outcome in itself: callers undo a whole set of commits at once, and only
/// the caller knows what that set was, so the record of the undoing belongs to it. This method only narrates.</para>
/// </remarks>
/// <inheritdoc/>
public void UndoLastCommit()
{
_reporter.Info("Undoing last commit...");
Expand All @@ -265,9 +203,7 @@ public void UndoLastCommit()
_repository.Reset(ResetMode.Hard, previousCommit);
}

/// <summary>
/// Pushes changes made to HEAD to the tracked remote. Fails if HEAD is not tracking any remote.
/// </summary>
/// <inheritdoc/>
public void Push(bool force = false)
{
var head = _repository.Head;
Expand Down
127 changes: 127 additions & 0 deletions src/Buildvana.Tool/Services/Git/IGitService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Buildvana.Tool.Infrastructure;
using JetBrains.Annotations;
using NuGet.Versioning;

namespace Buildvana.Tool.Services.Git;

/// <summary>
/// Provides shortcut methods to use the Git repository of the home directory.
/// </summary>
[PublicAPI]
internal interface IGitService
{
/// <summary>
/// Gets the name of the origin remote, i.e. either "origin" if such remote exists, or the name of the only remote if there is only one.
/// </summary>
string Origin { get; }

/// <summary>
/// Gets the fetch URL of the origin remote.
/// </summary>
Uri OriginUrl { get; }

/// <summary>
/// Gets the name of the current Git branch.
/// </summary>
/// <value>If HEAD is on a branch, the name of the branch; otherwise, the empty string.</value>
string CurrentBranch { get; }

/// <summary>
/// Gets or sets the identity of the Git committer.
/// </summary>
[DisallowNull]
GitIdentity? CommitterIdentity { get; set; }

/// <summary>
/// Gets or sets the credentials used for pushing to the Git repository if ambient credentials are not sufficient.
/// </summary>
/// <remarks>
/// <para>Set this property when ambient mechanisms (`http.extraheader` written by CI checkout actions,
/// URL-embedded credentials, OS credential helpers) are absent or insufficient.</para>
/// <para>The provided credentials are tried only after the server returns a 401 challenge to the initial push request.</para>
/// </remarks>
GitCredentials? PushCredentialsFallback { get; set; }

/// <summary>
/// Gets the SHA of the current <c>HEAD</c> commit.
/// </summary>
string HeadSha { get; }

/// <summary>
/// Tells whether a tag exists in the local Git repository.
/// </summary>
/// <param name="tag">The tag to check for.</param>
/// <returns>If a tag whose name is equal to <paramref name="tag"/> exists in the repository, <see langword="true"/>;
/// otherwise, <see langword="false"/>.</returns>
bool TagExists(string tag);

/// <summary>
/// Gets the latest version and the latest stable version in commit history.
/// </summary>
/// <returns>A tuple of the latest version and the latest stable version.</returns>
/// <remarks>
/// <para>If no version tag is found in commit history, this method returns a tuple of two <see langword="null"/>s.</para>
/// <para>If no stable version tag is found in commit history, this method returns a tuple of the latest
/// version and <see langword="null"/>.</para>
/// </remarks>
(SemanticVersion? Latest, SemanticVersion? LatestStable) GetLatestVersions();

/// <summary>
/// Runs an operation and detects the files it modified, by comparing working-tree snapshots
/// (as per <see cref="GetDirtyFiles"/>) taken before and after the operation.
/// </summary>
/// <typeparam name="T">The type of the operation's result.</typeparam>
/// <param name="operation">The operation to run.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the ongoing operation, whose result is a tuple
/// of the operation's result and the absolute paths of the files the operation modified. Files that
/// were already dirty before the operation are not reported, even if the operation modified them again.</returns>
Task<(T Result, IReadOnlyList<string> ChangedFiles)> TrackChangesAsync<T>(Func<Task<T>> operation);

/// <summary>
/// Gets the files that are new, modified, or otherwise different between the working directory
/// (including the index) and the current <c>HEAD</c> commit. Ignored files are not considered;
/// neither are files under bv's scratch directory (<see cref="CommonPaths.Scratch"/>),
/// whether or not the repository ignores it.
/// </summary>
/// <returns>The absolute paths of the differing files.</returns>
IReadOnlyList<string> GetDirtyFiles();

/// <summary>
/// Adds one or more files to the Git index.
/// </summary>
/// <param name="paths">The paths of the files to add.</param>
void Stage(params string[] paths);

/// <summary>
/// Commits staged changes, or amends last commit.
/// </summary>
/// <param name="message">The commit message.</param>
/// <param name="amend">If <see langword="true"/>, amends last commit instead of creating a new commit.</param>
/// <param name="allowEmpty">If <see langword="true"/>, allows creating (or amending into) an empty commit.</param>
void Commit(string message, bool amend = false, bool allowEmpty = false);

/// <summary>
/// Undoes the most recent commit.
/// </summary>
/// <remarks>
/// <para>This method's purpose is to undo a commit that was just generated by code and is not a merge commit.</para>
/// <para>If the current <c>HEAD</c> has multiple parents, the behavior of this method is undefined.</para>
/// <para>If the repository has no commits, or the current <c>HEAD</c> has no parents, this method will fail.</para>
/// <para>Undoing a commit is not an outcome in itself: callers undo a whole set of commits at once, and only
/// the caller knows what that set was, so the record of the undoing belongs to it. This method only narrates.</para>
/// </remarks>
void UndoLastCommit();

/// <summary>
/// Pushes changes made to HEAD to the tracked remote. Fails if HEAD is not tracking any remote.
/// </summary>
/// <param name="force">If <see langword="true"/>, force-pushes HEAD to the tracked remote.</param>
void Push(bool force = false);
}
Loading
Loading