Skip to content

[minor] Implement Commit and Push - #394

Merged
matt-edmondson merged 3 commits into
mainfrom
claude/record-issues-defects-n6p37v
Aug 28, 2026
Merged

[minor] Implement Commit and Push#394
matt-edmondson merged 3 commits into
mainfrom
claude/record-issues-defects-n6p37v

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #392.

Pull was wired up in #393; Commit and Push were left as disabled placeholders. They now work.

Commit

Committing stages everything, untracked files included, so the prompt lists the paths it is about to sweep up before asking for a message. That list is the only thing standing between the user and committing something they did not mean to, which is why it is shown rather than just a message box.

  • The list is capped at 20 paths, with the remainder summarised as ... and N more. A repository mid-rebuild can have thousands of pending paths, and a prompt taller than the display cannot be dismissed.
  • The pending paths are gathered before the prompt opens, so the list the user agreed to is the one they were shown. It is still advisory — the working tree can change while the prompt is up, and git remains the authority on what actually gets staged.
  • A clean tree logs "nothing to commit" instead of opening an empty prompt.
  • If git add --all fails, the commit is skipped rather than recording a subset of what the user was shown without saying so.

Push

A plain git push — no refspec, no force. The result goes through QueueGitLog, so a rejection surfaces in the log panel along with git's own explanation rather than being swallowed. Credentials come from the platform credential helper, as everywhere else here.

GitCli.ListPendingChanges

New, and the one non-obvious piece. It asks status --porcelain -z, so entries are NUL-separated and git applies none of the quoting it otherwise uses for unusual paths.

Each record is two status characters, a space, then the path. A rename or copy additionally emits its source path as a bare following entry with no status prefix. The loop therefore consumes that entry rather than testing every entry for a prefix — a source path such as ab cd.txt has a space in the third position and is indistinguishable from a record by inspection alone. Reporting it would show the user a file that no longer exists.

Tests

11 new cases in ProjectDirector.Test/CommitTests.cs, split the same way #393 split DecidePull: the parts with a rule in them are plain methods that can be driven without a live ImGui context or a display.

Against real throwaway repositories — clean tree, modified tracked file, untracked file, a path containing a space, a rename, a rename whose source looks like a record, and a directory that is not a repository at all. Against DescribePendingChanges — the singular case, a short list, exactly the cap (no ... and 0 more), and past the cap.

Suite goes 12 → 23. Verified load-bearing by substitution, re-checking the tree after each run:

Mutation Failures
Rename-source consumption disabled 1
Summary line fires at remaining >= 0 2
Plural rule shifted to Count == 2 1
Cap ignored, everything listed 1

Only the deliberately-crafted ab cd.txt rename catches the first one — a source path like tracked.txt has no space in the third position, so the naive prefix test happens to handle it. That is precisely why that test exists.


Generated by Claude Code

claude added 2 commits August 27, 2026 12:41
Commit and Push were disabled placeholders. They now work.

Commit stages everything, untracked files included, so the prompt lists the
paths it is about to sweep up before asking for a message -- that list is the
only thing standing between the user and committing something they did not
mean to. The list is capped at 20 paths, because a repository mid-rebuild can
have thousands and a prompt taller than the display cannot be dismissed. If
staging fails the commit is skipped rather than recording a subset of what the
user was shown.

Push is a plain `git push`, no refspec and no force, with the result piped
through QueueGitLog so a rejection appears in the log panel along with git's
own explanation instead of being swallowed.

GitCli.ListPendingChanges parses `status --porcelain -z` by consuming a
rename's trailing source entry rather than by testing each entry for a status
prefix. A source path such as "ab cd.txt" has a space in the third position and
is indistinguishable from a record by inspection, so a prefix test would list a
file that no longer exists.

Tests: 11 new cases covering what git reports as pending (clean tree, modified
tracked file, untracked file, a path with a space, a rename, a rename whose
source looks like a record, a non-repository) and how that is described
(singular, short list, exactly the cap, past the cap). Verified load-bearing by
substitution: disabling rename consumption fails 1, shifting the summary
threshold fails 2, shifting the plural rule fails 1, ignoring the cap fails 1.

Fixes #392

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTHNXgSNEHUSQ5KMLgivno
SonarCloud flagged 42.3% coverage on new code. The uncovered half was the
part that actually matters: whether Commit commits the right things and
whether Push sends them. Both were inline in a Task inside an ImGui
handler, so nothing could reach them.

GitCli.StageAllAndCommit and GitCli.Push now hold that work, and
CommitRepo/PushRepo are the thin wrappers that pipe results to the log
panel. The sequencing rule moves with them: a null commit result means
staging failed and the commit was never attempted, which is what
distinguishes that from git refusing a clean tree.

Six new tests against real throwaway repositories: a modified file ends
up in a commit with the right message and a clean tree after; add --all
sweeps up an untracked file; a clean tree fails the commit without moving
HEAD; a failed stage yields no commit result at all; pushing lands the
commit in a bare repository standing in for a remote (real push
negotiation, no network, no credentials); and a branch with no upstream
fails carrying git's own explanation.

Verified load-bearing by substitution: inverting the stage/commit
condition fails 5, weakening add --all to add --update fails 1, and
turning Push into fetch fails 2.

Suite 23 -> 29.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTHNXgSNEHUSQ5KMLgivno

Copy link
Copy Markdown
Contributor Author

SonarCloud failed this at 42.3% coverage on new code (gate wants ≥80%). Pushed ed5fc9e in response, but the fix was a real gap rather than a number-chase, so worth saying what it was.

The uncovered half was the part that actually matters. DescribePendingChanges and ListPendingChanges were tested; the git work — does Commit commit the right things, does Push send them — was inline in a Task inside an ImGui handler, so nothing could reach it. The tested code was the prompt's wording; the untested code was the behaviour.

GitCli.StageAllAndCommit and GitCli.Push now hold that work, and CommitRepo/PushRepo are thin wrappers that pipe the results to the log panel. The sequencing rule moved with it: a null commit result means staging failed and the commit was never attempted, which is what distinguishes it from git refusing a clean tree — those are different things and the log panel should not conflate them.

Six new tests against real throwaway repositories:

  • a modified file ends up in a commit, with the right message and a clean tree afterwards
  • add --all genuinely sweeps up an untracked file — the behaviour the prompt's file list warns about, now pinned
  • a clean tree fails the commit without moving HEAD
  • a failed stage yields no commit result at all
  • pushing lands the commit in a bare repository standing in for a remote — real push negotiation, no network, no credentials
  • a branch with no upstream fails, carrying git's own explanation rather than a swallowed exit code

Suite 23 → 29. Verified load-bearing by substitution, re-checking the tree after each run: inverting the stage/commit condition fails 5, weakening add --all to add --update fails 1, turning Push into fetch fails 2.

I can't read the new percentage from here — sonarcloud.io is egress-blocked in this sandbox — so I don't know whether this clears 80%. What remains uncovered is CommitRepoAfterConfirmation's popup wiring, the two Task wrappers, and the button block, none of which can be driven without a live ImGui context and a display; this repo has no headless harness (ImGuiApp has one, ProjectDirector doesn't). If it still falls short, that residue is the reason, and the honest options are a headless harness for this app or a coverage exclusion for the drawing layer — not more tests around it. Say which you'd prefer and I'll do it.


Generated by Claude Code

Found while reading back the uncovered half of this diff. The Task body
chose between "Staging X failed" and "Committing X failed" inline, so
nothing could test it, and getting it backwards is not cosmetic: a user
whose git add failed would be told the commit failed and go looking at
the wrong step.

DescribeCommitOutcome now holds that choice and two tests pin it.
Substituting the description text fails 1; inverting the null test does
not compile at all, because the nullable commit result cannot flow into
the non-nullable tuple -- the compiler already forbids the more serious
half of this mistake.

Suite 29 -> 31.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTHNXgSNEHUSQ5KMLgivno
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
50.9% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment: I asked you to choose between a headless harness and a coverage exclusion. Neither is needed — this PR isn't blocked.

.NET Workflow concluded success on d67727b, as did every other check. The only red signal is SonarCloud's own check run, and the shared dotnet.yml template only passes sonar.qualitygate.wait=true where a repository sets the SONAR_BLOCKING_GATE variable — which this one doesn't. The template's own comment says why: most of these repos carry untriaged security hotspots, so a gate they've never been held to would stop every release at once. The analysis still uploads and the gate is still evaluated; it just doesn't block. I read "Quality Gate failed" as a blocking failure without checking whether the job it belongs to actually failed.

Coverage on new code went 42.3% → 46.4% → 50.9% across the three pushes, and the two after the first were worth making on their own merit — the git work was genuinely untested, and so was the rule choosing whether a failure gets reported as staging or committing. But I should not have framed them as clearing a gate, and I'm stopping here rather than pushing further at the number.

For the record, since it may matter when you do decide to turn the gate on somewhere: a per-repo exclusion isn't available as an option. sonar.coverage.exclusions lives in the templated dotnet.yml, byte-identical across every repo I checked, so changing it is a change to all ~57. The remaining uncovered lines here are the popup wiring, the two Task wrappers and the button block — all of which need a live ImGui context and a display.


Generated by Claude Code

@matt-edmondson
matt-edmondson merged commit 97e28a5 into main Aug 28, 2026
11 of 12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/record-issues-defects-n6p37v branch August 28, 2026 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement the Commit and Push actions

2 participants