[minor] Implement Commit and Push - #394
Conversation
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
|
SonarCloud failed this at 42.3% coverage on new code (gate wants ≥80%). Pushed The uncovered half was the part that actually matters.
Six new tests against real throwaway repositories:
Suite 23 → 29. Verified load-bearing by substitution, re-checking the tree after each run: inverting the stage/commit condition fails 5, weakening I can't read the new percentage from here — 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
|
|
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.
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. Generated by Claude Code |


Fixes #392.
Pullwas wired up in #393;CommitandPushwere 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.
... and N more. A repository mid-rebuild can have thousands of pending paths, and a prompt taller than the display cannot be dismissed.git add --allfails, 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 throughQueueGitLog, 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.ListPendingChangesNew, 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.txthas 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 splitDecidePull: 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:
remaining >= 0Count == 2Only the deliberately-crafted
ab cd.txtrename catches the first one — a source path liketracked.txthas 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