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 CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,12 @@ Non-obvious, load-bearing design points:
leaving a host-configurable default unspecified. `WithUntrackedFiles(...)` replaces the default
rather than joining it, so the vector never carries two `--untracked-files` values.

15. **`Log()` passes `--no-show-signature`.** With `log.showSignature=true`, which is common among
people who sign commits, git prints each signed commit's verification to standard output ahead
of the record even under a custom `--format`, and the parser reads it as part of the commit id.
`Commit()` reuses the `Log()` vector for its read-back, so without the flag a signed commit that
git did make is reported to the caller as a failure, and a retry makes a duplicate.

**Hosting layer.** `GitProvider` is an abstract base with two implementations: `GitHubProvider` over
Octokit, and `AzureDevOpsProvider` over a raw `HttpClient` — Azure DevOps has no client library this
library uses (see the dependency note below). Both go through the same shape: every request-issuing
Expand Down
15 changes: 15 additions & 0 deletions GitIntegration.Test/Builders/GitCommitBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@
CollectionAssert.Contains(readBack, "-z");
}

[TestMethod]
public async Task TheReadBackSuppressesSignatureOutputAsync()
{
// With log.showSignature=true git prints the new commit's signature check ahead of the record,
// so a signed commit that git made would be reported to the caller as a failure.
ScriptedGitProcessRunner runner = new ScriptedGitProcessRunner()
.Then(standardOutput: "[main 9429d20] subject here\n")
.Then(standardOutput: ReadBack);
GitCommitBuilder builder = new(runner, TestPaths.Root, Message);

_ = await builder.ExecuteAsync(TestContext.CancellationTokenSource.Token).ConfigureAwait(false);

Check warning on line 166 in GitIntegration.Test/Builders/GitCommitBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'TestContext.CancellationToken' instead of 'TestContext.CancellationTokenSource.Token'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaEQ_7IC4CMLCseAnHcu&open=AaEQ_7IC4CMLCseAnHcu&pullRequest=179

CollectionAssert.Contains(runner.Invocations[1].ToArray(), "--no-show-signature");

Check warning on line 168 in GitIntegration.Test/Builders/GitCommitBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'CollectionAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaEQ_7IC4CMLCseAnHct&open=AaEQ_7IC4CMLCseAnHct&pullRequest=179
}

[TestMethod]
public async Task ThrowsNothingToCommitWhenTheTreeIsCleanAsync()
{
Expand Down
1 change: 1 addition & 0 deletions GitIntegration.Test/Builders/GitLogBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public void BuildsTheDefaultLogVector()
"-c", "core.quotepath=false",
"-c", "color.ui=false",
"log",
"--no-show-signature",
"-z",
ExpectedFormat,
];
Expand Down
52 changes: 52 additions & 0 deletions GitIntegration.Test/Integration/GitRoundTripTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,58 @@
Assert.AreEqual(40, commit.Sha.WeakString.Length);
}

[TestMethod]
public async Task LogAndCommitIgnoreTheHostsShowSignatureSettingAsync()
{
// log.showSignature is common among people who sign commits, and with it git prints each signed
// commit's verification ahead of the record, custom --format or not. The signature here is
// forged rather than made, so the test needs no key: git still hands it to gpg and prints what
// gpg says about it, which is all it takes to put text where the parser expects a commit id.
CancellationToken cancellationToken = TestContext.CancellationTokenSource.Token;

Check warning on line 121 in GitIntegration.Test/Integration/GitRoundTripTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'TestContext.CancellationToken' instead of 'TestContext.CancellationTokenSource.Token'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaEQ_7ON4CMLCseAnHcv&open=AaEQ_7ON4CMLCseAnHcv&pullRequest=179
await IntegrationGitFixture.RequireGitAsync(cancellationToken).ConfigureAwait(false);

using TemporaryRepository temporary = new();
GitRepository repository = await InitializeAsync(temporary, cancellationToken).ConfigureAwait(false);

temporary.WriteFile("a.txt", "one\n");
_ = await repository.Add().All().ExecuteAsync(cancellationToken).ConfigureAwait(false);
_ = await repository.Commit("c1".As<GitCommitMessage>()).ExecuteAsync(cancellationToken).ConfigureAwait(false);

IGitProcessRunner runner = repository.ProcessRunner!;
string unsigned = await new GitTextBuilder(runner, repository.LocalPath, "cat-file", "commit", "HEAD")
.ExecuteAsync(cancellationToken).ConfigureAwait(false);

// The header block ends at the first blank line; gpgsig goes last in it, as git writes it.
int headerEnd = unsigned.IndexOf("\n\n", StringComparison.Ordinal);
string signed = unsigned[..headerEnd] +
"\ngpgsig -----BEGIN PGP SIGNATURE-----\n \n iQEzBAABCAAdFiEEAAAAAAAAAAAAAAAAAAAAAAAAAAAFAmYAAAAACgkQAAAAAAAAAAAA\n" +
" =AAAA\n -----END PGP SIGNATURE-----" +
unsigned[headerEnd..] + "\n";

// Written inside .git so the forged object never shows up as working-tree content.
temporary.WriteFile(".git/forged-commit", signed);
string forgedSha = await new GitTextBuilder(
runner, repository.LocalPath, "hash-object", "-t", "commit", "-w", ".git/forged-commit")
.ExecuteAsync(cancellationToken).ConfigureAwait(false);
_ = await new GitTextBuilder(runner, repository.LocalPath, "update-ref", "HEAD", forgedSha)
.ExecuteAsync(cancellationToken).ConfigureAwait(false);
_ = await new GitTextBuilder(runner, repository.LocalPath, "config", "log.showSignature", "true")
.ExecuteAsync(cancellationToken).ConfigureAwait(false);

IReadOnlyList<GitCommit> history = await repository.Log().ExecuteAsync(cancellationToken).ConfigureAwait(false);

Assert.AreEqual(1, history.Count);

Check warning on line 154 in GitIntegration.Test/Integration/GitRoundTripTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.HasCount' instead of 'Assert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitIntegration&issues=AaEQ_7ON4CMLCseAnHcw&open=AaEQ_7ON4CMLCseAnHcw&pullRequest=179
Assert.AreEqual(forgedSha, history[0].Sha.WeakString);

temporary.WriteFile("b.txt", "two\n");
_ = await repository.Add().All().ExecuteAsync(cancellationToken).ConfigureAwait(false);
GitCommit second = await repository.Commit("c2".As<GitCommitMessage>())
.ExecuteAsync(cancellationToken).ConfigureAwait(false);

Assert.AreEqual("c2", second.Subject);
Assert.AreEqual(forgedSha, second.ParentShas.Single().WeakString);
}

[TestMethod]
public async Task CommittingWithNothingStagedThrowsTheDedicatedExceptionAsync()
{
Expand Down
5 changes: 5 additions & 0 deletions GitIntegration/Builders/GitLogBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ protected override void AppendVerbArguments(ICollection<string> arguments)
Ensure.NotNull(arguments);

arguments.Add("log");

// log.showSignature makes git print each commit's signature check to standard output ahead of
// the record, custom --format or not, which the parser would read as part of the commit id.
// Commit() reuses this vector for its read-back, so the same flag keeps it working too.
arguments.Add("--no-show-signature");
arguments.Add("-z");
arguments.Add("--format=" + GitOutputFormats.LogFormat);

Expand Down
Loading