From c81fbc79368182fddca77e7fde17d29b6f53cca8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 03:26:01 +0000 Subject: [PATCH] Keep "#" lines in a commit message where the host strips comments [patch] Commit() never pinned --cleanup, so commit.cleanup=strip (or scissors) in the host's git config made git drop every "#"-leading line of the message: "#123 fixed the crash" vanished from the body without a word, and a "#42: fix login" subject aborted the commit as empty. Always pass --cleanup=whitespace, which is git's own default for a message that never opens an editor, so unconfigured hosts store the same message as before. Fixes #140 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NwJrDAUtukhDgLo9pAfR3t --- CLAUDE.md | 6 ++++ .../Builders/GitCommitBuilderTests.cs | 17 ++++++++++ .../Integration/GitRoundTripTests.cs | 34 +++++++++++++++++++ GitIntegration/Builders/GitCommitBuilder.cs | 6 ++++ 4 files changed, 63 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 777cc31..316fb9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -366,6 +366,12 @@ Non-obvious, load-bearing design points: `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. +16. **`Commit()` passes `--cleanup=whitespace`.** With `commit.cleanup=strip` (or `scissors`) in the + host's config, git treats every `#`-leading line of a `--message` as a comment and drops it, so + `#123 fixed the crash` silently vanishes from the body, and a `#42: fix login` subject aborts the + commit as empty. `whitespace` is git's own default for a message that never opens an editor, so + the stored message on an unconfigured host is unchanged. + **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 diff --git a/GitIntegration.Test/Builders/GitCommitBuilderTests.cs b/GitIntegration.Test/Builders/GitCommitBuilderTests.cs index 0496962..df02551 100644 --- a/GitIntegration.Test/Builders/GitCommitBuilderTests.cs +++ b/GitIntegration.Test/Builders/GitCommitBuilderTests.cs @@ -39,6 +39,7 @@ public void BuildsTheDefaultCommitVector() "-c", "core.quotepath=false", "-c", "color.ui=false", "commit", + "--cleanup=whitespace", "--message", "subject here", ]; CollectionAssert.AreEqual(expectedArguments, builder.BuildArguments().ToArray()); @@ -61,6 +62,22 @@ public void EmitsTheBodyAsASecondMessage() Assert.AreEqual("body text", arguments[subject + 2]); } + [TestMethod] + public void PinsTheMessageCleanupModeAheadOfTheMessage() + { + // commit.cleanup=strip on the host would otherwise drop every "#"-leading line of the message. + RecordingGitProcessRunner runner = new(); + GitCommitBuilder builder = new(runner, TestPaths.Root, Message); + + _ = builder.WithBody("#123 fixed the crash").AllowEmpty().StageTrackedFiles(); + + string[] arguments = [.. builder.BuildArguments()]; + Assert.AreEqual(1, Array.FindAll(arguments, a => a.StartsWith("--cleanup", StringComparison.Ordinal)).Length); + Assert.IsLessThan( + Array.IndexOf(arguments, "--message"), + Array.IndexOf(arguments, "--cleanup=whitespace")); + } + [TestMethod] public void MapsTheOptionFlags() { diff --git a/GitIntegration.Test/Integration/GitRoundTripTests.cs b/GitIntegration.Test/Integration/GitRoundTripTests.cs index 6e4e5e5..ce55b5e 100644 --- a/GitIntegration.Test/Integration/GitRoundTripTests.cs +++ b/GitIntegration.Test/Integration/GitRoundTripTests.cs @@ -361,6 +361,40 @@ public async Task StatusReportsUntrackedWorkEvenWhereTheHostHidesItAsync() Assert.IsTrue(suppressed.IsClean); } + [TestMethod] + public async Task CommitKeepsHashLinesEvenWhereTheHostStripsCommentsAsync() + { + // commit.cleanup is a host setting, and under "strip" git treats every "#"-leading line as a + // comment. Written into the throwaway repository's own config for the same reason as the + // status.showUntrackedFiles test above: same variable, nearest scope, host left alone. + CancellationToken cancellationToken = TestContext.CancellationTokenSource.Token; + await IntegrationGitFixture.RequireGitAsync(cancellationToken).ConfigureAwait(false); + + using TemporaryRepository temporary = new(); + GitRepository repository = await InitializeAsync(temporary, cancellationToken).ConfigureAwait(false); + + _ = await new GitTextBuilder( + repository.ProcessRunner!, repository.LocalPath, "config", "commit.cleanup", "strip") + .ExecuteAsync(cancellationToken).ConfigureAwait(false); + + temporary.WriteFile("a.txt", "one\n"); + _ = await repository.Add().All().ExecuteAsync(cancellationToken).ConfigureAwait(false); + GitCommit commit = await repository.Commit("Subject".As()) + .WithBody("#123 fixed the crash\nsecond") + .ExecuteAsync(cancellationToken).ConfigureAwait(false); + + Assert.AreEqual("#123 fixed the crash\nsecond", commit.Body, "Commit() dropped the body's \"#\" line."); + + // A "#"-leading subject is the whole message as far as strip is concerned; without the pin + // git aborts with "empty commit message" instead of recording it. + temporary.WriteFile("a.txt", "two\n"); + _ = await repository.Add().All().ExecuteAsync(cancellationToken).ConfigureAwait(false); + GitCommit hashSubject = await repository.Commit("#42: fix login".As()) + .ExecuteAsync(cancellationToken).ConfigureAwait(false); + + Assert.AreEqual("#42: fix login", hashSubject.Subject); + } + [TestMethod] public async Task BranchCreateCheckoutAndDeleteRoundTripAsync() { diff --git a/GitIntegration/Builders/GitCommitBuilder.cs b/GitIntegration/Builders/GitCommitBuilder.cs index feb8be7..a0d1625 100644 --- a/GitIntegration/Builders/GitCommitBuilder.cs +++ b/GitIntegration/Builders/GitCommitBuilder.cs @@ -129,6 +129,12 @@ protected override void AppendVerbArguments(ICollection arguments) arguments.Add("--author=" + _author); } + // Pinned because commit.cleanup is a host setting: under "strip" or "scissors" git treats every + // "#"-leading line as a comment and drops it, so "#123 fixed the crash" vanishes from the body + // and a "#42: fix login" subject aborts the commit as empty. "whitespace" is git's own default + // for a message that never opens an editor, so an unconfigured host stores the same message. + arguments.Add("--cleanup=whitespace"); + // --message rather than -m, and repeated for the body: git joins repeated values with a // blank line between them, which is precisely the subject-then-body convention. arguments.Add("--message");