Skip to content

feat(zerogit): auto-create a conventional branch before push/pr on default branch - #671

Open
euxaristia wants to merge 18 commits into
Gitlawb:mainfrom
euxaristia:euxaristia/auto-branch-naming-for-changes-workflow
Open

feat(zerogit): auto-create a conventional branch before push/pr on default branch#671
euxaristia wants to merge 18 commits into
Gitlawb:mainfrom
euxaristia:euxaristia/auto-branch-naming-for-changes-workflow

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • zero changes push and zero changes pr currently refuse to push straight to the default branch (main/master) but don't offer any alternative, so hitting that guard is a dead end.
  • Added CreateBranch, IsDefaultBranch, CurrentGitUser, SlugifyBranchComponent, and BuildBranchName to internal/zerogit.
  • push/pr now call a new ensureFeatureBranch step: if the current branch is the default branch and neither --yes nor --dry-run was passed, it generates a short slug for the diff (via the configured LLM provider, falling back to a deterministic slug derived from the changed files if no provider is configured) and checks out <git user>/<slug> before pushing. --yes and --dry-run bypass this entirely, preserving the existing refuse/preview behavior.

Linked issue

None. This came out of a direct discussion about zero having no defined branch-naming convention, not a filed issue.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./... (all green except a pre-existing, unrelated failure in internal/contextreport that also fails on unmodified main)
  • gofmt -l clean on all changed files
  • New unit tests in internal/zerogit/zerogit_test.go (CreateBranch, IsDefaultBranch, CurrentGitUser, SlugifyBranchComponent, BuildBranchName)
  • New CLI-level tests in internal/cli/workflow_test.go covering ensureFeatureBranch directly (default-branch creation with/without a provider, skip when already off default, skip on --yes/--dry-run) and end-to-end through zero changes push

Summary by CodeRabbit

  • New Features
    • changes push and changes pr now auto-create and use a non-default feature branch when invoked from the default branch, with remote resolution and preflight inspection.
    • --auto generates AI-assisted commit messages and feature-branch naming, including normalization of inconsistent responses.
    • --diff-bytes is respected during inspection.
  • Bug Fixes
    • Default-branch protection is stricter and fail-closed unless overridden with --yes, and pushes won’t overwrite existing remote branches.
    • PR creation is blocked when the target remote is confirmed unborn (with guidance to push first).
  • Documentation
    • Updated help and argument validation for --auto/--message and related flags.
  • Tests
    • Expanded coverage for branch creation, slug parsing/normalization, preflight decisions, and failure edge cases.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds fail-closed Git branch detection and creation APIs, then wires automatic feature-branch selection into changes push and changes pr. Branch names derive from inspected changes, commit subjects, or sanitized LLM output, with expanded validation and workflow coverage.

Changes

Automatic feature branch routing

Layer / File(s) Summary
Git branch management APIs
internal/zerogit/zerogit.go, internal/zerogit/zerogit_test.go
Adds default-branch detection, collision-aware branch creation, Git identity and slug helpers, protected push behavior, generated-branch markers, and corresponding tests.
CLI Git dependency wiring
internal/cli/app.go
Adds injectable Git operations and fills them from zerogit by default.
Feature branch generation
internal/cli/workflows.go, internal/cli/workflow_test.go
Adds default-branch checks, tracking refresh, ahead-count validation, deterministic or provider-generated slugs, commit-subject fallback naming, argument validation, and focused tests.
Push and PR workflow integration
internal/cli/workflows.go, internal/cli/workflow_test.go
Passes ensured branches and resolved remotes into push and PR flows, including new-remote protection, retry behavior, and --yes handling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant runChangesPush
  participant ensureFeatureBranch
  participant zerogit
  participant Push
  User->>runChangesPush: invoke changes push
  runChangesPush->>ensureFeatureBranch: determine branch target
  ensureFeatureBranch->>zerogit: resolve default branch and ahead count
  ensureFeatureBranch->>zerogit: create computed feature branch
  runChangesPush->>Push: push branch to resolved remote
Loading

Possibly related PRs

  • Gitlawb/zero#391: Adds the underlying changes push/pr and push flow that this feature-branch routing extends.
  • Gitlawb/zero#423: Modifies the related changes argument parsing and --auto handling extended by this change.

Suggested reviewers: gnanam1990, kevincodex1, vasanthdev2004

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: automatically creating a conventional branch before push/pr on the default branch.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
internal/zerogit/zerogit_test.go (2)

836-848: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

CurrentGitUser's fallback branches (OS username, literal "user") are untested.

Only the git config user.name success path is covered. The two fallback tiers (L715-718 in zerogit.go) have no coverage, at least a case where the fake runner errors/returns empty output to exercise the OS-username path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/zerogit/zerogit_test.go` around lines 836 - 848, Extend
TestCurrentGitUser to cover CurrentGitUser’s fallback behavior when git config
returns an error or empty output, asserting the OS-username fallback and command
invocation; also add coverage for the final literal "user" fallback when no OS
username is available.

731-792: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing coverage for CreateBranch failing to check out (e.g. branch already exists).

All three subtests exercise success/dry-run/empty-name paths; none exercise the git checkout -b failure branch (L700-702 in zerogit.go), which is exactly the scenario that occurs on a name collision. Worth adding a subtest that returns a non-nil error/non-zero exit from the checkout call and asserts the wrapped error message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/zerogit/zerogit_test.go` around lines 731 - 792, The
TestCreateBranch coverage should include checkout failure, such as an existing
branch name. Add a subtest alongside HappyPath and DryRunDoesNotCheckout using
fakeRunner to return a non-zero/error result for the checkout invocation, then
assert CreateBranch returns an error containing the wrapped checkout failure
message.
internal/cli/workflows.go (1)

1029-1060: 🚀 Performance & Scalability | 🔵 Trivial

Extra LLM round-trip added to every default-branch push/PR without --yes.

When on the default branch (and no --yes/dry-run), ensureFeatureBranch now performs its own StreamCompletion call to name the branch, on top of any existing commit-message-generation LLM call in the same flow. That's an additional ~60s-capped network round trip and provider cost on a very common path (any push straight from main). Worth being aware of for latency/cost budgeting, and consider whether the fallback slug is "good enough" to skip the LLM call by default (e.g., behind a flag) rather than always attempting it whenever a provider is configured.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflows.go` around lines 1029 - 1060, The ensureFeatureBranch
flow should not automatically perform an LLM request on every default-branch
push or PR. Use fallbackBranchSlug(summary) by default and gate
generateAutoBranchSlug behind an explicit opt-in configuration or flag,
preserving the existing branch-generation behavior when that opt-in is enabled.
internal/cli/workflow_test.go (1)

918-980: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No CLI-level test for runChangesPR + ensureFeatureBranch integration.

Coverage is added for ensureFeatureBranch in isolation and for runChangesPush (Lines 918-980), but runChangesPR also now routes through ensureFeatureBranch and forwards the ensured branch into PushOptions.Branch (workflows.go Lines 924-928, 937). Given pr hardcodes dryRun=false unlike push, this path deserves its own targeted test (e.g. mirroring TestRunChangesPushCreatesFeatureBranchWhenOnDefault for changes pr on the default branch) to lock in the new behavior before it ships.

Want me to draft that test?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` around lines 918 - 980, The CLI tests cover
feature-branch creation for runChangesPush but not the equivalent runChangesPR
flow. Add a targeted test for changes pr on the default branch, verifying
ensureFeatureBranch creates the expected branch and that runChangesPR forwards
it through PushOptions.Branch, while preserving the existing dryRun=false
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/workflows.go`:
- Around line 1087-1116: Update generateAutoBranchSlug to normalize
collected.Text before passing it to zerogit.SlugifyBranchComponent: select the
first non-empty line, trim surrounding whitespace and quotes, then slugify that
single-line value. Preserve the existing empty-slug error handling and provider
error propagation.

In `@internal/zerogit/zerogit.go`:
- Around line 637-662: Bound the remote lookup performed by IsDefaultBranch,
especially the isDefaultBranch call that may execute git ls-remote, with a short
context timeout even when the caller supplies context.Background(). On timeout
or remote lookup failure, preserve the existing local main/master fallback so
changes push/pr do not stall.
- Around line 677-704: Update CreateBranch to handle an already-existing local
branch before treating branch creation as failed: detect whether the requested
name exists locally, check it out and return the same BranchResult when it does,
while retaining checkout -b for new branches and preserving DryRun behavior.

---

Nitpick comments:
In `@internal/cli/workflow_test.go`:
- Around line 918-980: The CLI tests cover feature-branch creation for
runChangesPush but not the equivalent runChangesPR flow. Add a targeted test for
changes pr on the default branch, verifying ensureFeatureBranch creates the
expected branch and that runChangesPR forwards it through PushOptions.Branch,
while preserving the existing dryRun=false behavior.

In `@internal/cli/workflows.go`:
- Around line 1029-1060: The ensureFeatureBranch flow should not automatically
perform an LLM request on every default-branch push or PR. Use
fallbackBranchSlug(summary) by default and gate generateAutoBranchSlug behind an
explicit opt-in configuration or flag, preserving the existing branch-generation
behavior when that opt-in is enabled.

In `@internal/zerogit/zerogit_test.go`:
- Around line 836-848: Extend TestCurrentGitUser to cover CurrentGitUser’s
fallback behavior when git config returns an error or empty output, asserting
the OS-username fallback and command invocation; also add coverage for the final
literal "user" fallback when no OS username is available.
- Around line 731-792: The TestCreateBranch coverage should include checkout
failure, such as an existing branch name. Add a subtest alongside HappyPath and
DryRunDoesNotCheckout using fakeRunner to return a non-zero/error result for the
checkout invocation, then assert CreateBranch returns an error containing the
wrapped checkout failure message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f58092c2-d708-4e9f-b966-136d674d41ae

📥 Commits

Reviewing files that changed from the base of the PR and between 80c39aa and 39cfc2e.

📒 Files selected for processing (5)
  • internal/cli/app.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/zerogit/zerogit.go
  • internal/zerogit/zerogit_test.go

Comment thread internal/cli/workflows.go
Comment thread internal/zerogit/zerogit.go Outdated
Comment thread internal/zerogit/zerogit.go
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed the review, including the nitpicks.

Actionable:

  • generateAutoBranchSlug now takes the first non-empty, quote-trimmed line of the model's response before slugifying, instead of folding any preamble or wrapping quotes into the branch name.
  • isDefaultBranch's ls-remote lookup is now bounded by a 5s timeout, so a slow or unreachable remote can't stall push/pr; falls back to the local main/master heuristic as before.
  • CreateBranch now checks whether the target branch already exists locally and checks it out instead of failing checkout -b on the collision.

Nitpicks fixed:

  • Added coverage for CurrentGitUser's OS-username fallback tier (the third "literal user" tier isn't independently testable without adding an injection seam to os/user.Current, noted in the test comment).
  • Added coverage for CreateBranch's new existing-branch path and a genuine checkout -b failure.
  • Added a CLI-level test for changes pr + ensureFeatureBranch (there was no changes pr coverage at all before this), mirroring the existing changes push test.

One nitpick I'm leaving as-is: the extra LLM round-trip on every default-branch push without --yes. That's the intended behavior, not a bug: the LLM slug is the actual point of this feature, --yes/--dry-run already bypass it for anyone who wants to skip it, and gating it behind a further flag is new config surface for a cost/latency observation rather than a correctness issue.

go vet, go build ./..., and go test -race -count=1 ./internal/zerogit/... ./internal/cli/... are all clean.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 14, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Keep branch generation from selecting an unrelated local branch
    internal/cli/workflows.go:1042-1063, internal/zerogit/zerogit.go:709-718
    changes push normally runs after changes commit, so the working tree inspected here is clean and the fallback name is always user/changes (the provider path likewise receives an empty diff). On a later push, or whenever a low-entropy fallback/LLM name already exists locally, CreateBranch checks out that arbitrary existing ref instead of creating a branch at the current default-branch HEAD. The subsequent push/PR then publishes the stale branch while leaving the user's new commit on main. Do not treat a name collision as a retry unless its history is proven to be the intended current work; use a unique name or fail visibly, and cover the ordinary commit-then-push sequence.

  • [P1] Preserve and use the actual target remote throughout auto-branching
    internal/cli/workflows.go:866-878, internal/cli/workflows.go:924-940, internal/cli/workflows.go:1034, internal/zerogit/zerogit.go:574-585
    The preflight always checks origin, while the original push may target --remote or the current branch's configured upstream. After creating a branch, that new branch has no tracking configuration, so Push falls back to origin as well. In a fork/upstream setup this can either leave the advertised default-branch dead end intact (the target remote identifies the branch as default after the origin preflight skipped creation) or push/create the PR against origin rather than the source branch's upstream. Resolve the remote once before branching, pass it to IsDefaultBranch, and pass the same resolved remote to Push; add non-origin/upstream coverage.

  • [P1] Do not fail open when default-branch lookup times out
    internal/zerogit/zerogit.go:616-629
    The new five-second deadline applies to the existing guard in Push as well as the new preflight. If ls-remote --symref takes longer than five seconds for a repository whose default is trunk/develop, the fallback only recognizes main and master; Push then proceeds without --yes. Before this change it waited for the remote answer and preserved the confirmation guard. A lookup timeout needs to fail closed (or use a trusted local default reference), not be treated as evidence that an arbitrary branch is unprotected.

  • [P1] Do not upload a diff during ordinary push/PR without an explicit opt-in
    internal/cli/workflows.go:1048-1055
    A configured provider now causes every default-branch changes push/changes pr to send the full change diff to that provider. These commands were previously git-only; the existing LLM behavior is explicitly requested through changes commit --auto. redactChangeSummary removes secret-shaped values but intentionally retains ordinary source code, so this silently exports proprietary code (and JSON mode gives no notice). Keep the deterministic local naming path by default and require an explicit LLM opt-in before constructing this completion request.

@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed e3ec76f (plus gofmt fixup 68bcce3) for all four findings.

  • Branch collisions: CreateBranch no longer checks out an existing branch under the generated name. It picks a unique suffixed name at the current HEAD (name-2 through name-9) and fails visibly when the namespace is exhausted, so an old branch with unrelated history can no longer be published while the new commit stays behind on main. The ordinary commit-then-push sequence is also covered now: with a clean tree the fallback name comes from the HEAD commit subject instead of the constant user/changes, so repeat pushes stop colliding on the same low-entropy name in the first place. Covered by TestCreateBranch/SuffixesNameInsteadOfCheckingOutExistingBranch, FailsVisiblyWhenSuffixNamespaceExhausted, and TestEnsureFeatureBranchNamesFromHeadCommitAfterCommit.
  • Remote resolution: IsDefaultBranch now resolves the remote the same way Push does (explicit --remote, then the branch's configured upstream, then origin) and reports it; ensureFeatureBranch resolves it once against the original branch and push/pr thread it into Push, so a freshly created branch with no tracking configuration no longer silently retargets origin in fork setups. Covered by TestIsDefaultBranch/ResolvesRemoteFromBranchUpstream and TestRunChangesPushUsesResolvedRemoteForNewBranch.
  • Fail closed: main and master count as default with no network at all (the safe direction). When the remote lookup fails, the check falls back to the local refs/remotes//HEAD record, which needs no network; if that record is also missing, the check errors with guidance (git remote set-head) instead of silently downgrading to the main/master heuristic and dropping the guard for trunk/develop repositories. Covered by TestIsDefaultBranch/FallsBackToLocalRemoteHeadRecord and FailsClosedWhenDefaultBranchUnknown.
  • LLM opt-in: branch naming only calls the provider when --auto is passed to push/pr, mirroring commit --auto, and only when the diff is non-empty. A configured provider alone no longer causes the change diff to be uploaded during ordinary pushes; without the flag the name is derived from deterministic local information. Covered by TestEnsureFeatureBranchDoesNotCallProviderWithoutAuto; the help text documents the flag's new scope.

go build, go vet, and the zerogit and cli suites pass locally.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/zerogit/zerogit_test.go (1)

527-668: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for Push's new fail-closed error path.

TestPushBranchesToRemote exercises isDefault == true (RejectsDefaultBranch) and isDefault == false (HappyPath, FlagsForceAndDryRun, FallbackRemoteToOrigin), but none of the subtests drive isDefaultBranch into returning an error (remote lookup fails and no local refs/remotes/<remote>/HEAD record) to verify Push's new wrapping at Line 585-587 ("cannot verify %q is not the default/protected branch: %w; use --yes to override"). This is the actual new logic added to Push in this PR and is otherwise only exercised indirectly via IsDefaultBranch's own FailsClosedWhenDefaultBranchUnknown test, which doesn't go through Push.

✅ Suggested additional subtest
t.Run("FailsWhenDefaultBranchCannotBeVerified", func(t *testing.T) {
    root := t.TempDir()
    runner := &fakeRunner{results: []CommandResult{
        {Stdout: root + "\n"},
        {Stdout: "feat/some-feature\n"},
        {Stdout: "origin\n"},
        {ExitCode: 128, Stderr: "fatal:"}, // ls-remote fails
        {ExitCode: 1},                     // no local refs/remotes/origin/HEAD record
    }}

    _, err := Push(context.Background(), PushOptions{
        Cwd:    root,
        RunGit: runner.Run,
    })
    if err == nil || !strings.Contains(err.Error(), "use --yes to override") {
        t.Fatalf("expected fail-closed error, got %v", err)
    }
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/zerogit/zerogit_test.go` around lines 527 - 668, Add a
FailsWhenDefaultBranchCannotBeVerified subtest to TestPushBranchesToRemote that
makes ls-remote fail and the local refs/remotes/origin/HEAD lookup fail, then
assert Push returns an error containing “use --yes to override.” Ensure the fake
runner sequence reaches Push’s wrapped isDefaultBranch error path rather than
the existing successful or protected-branch paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/zerogit/zerogit_test.go`:
- Around line 527-668: Add a FailsWhenDefaultBranchCannotBeVerified subtest to
TestPushBranchesToRemote that makes ls-remote fail and the local
refs/remotes/origin/HEAD lookup fail, then assert Push returns an error
containing “use --yes to override.” Ensure the fake runner sequence reaches
Push’s wrapped isDefaultBranch error path rather than the existing successful or
protected-branch paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 16f16bb3-6e00-490a-b235-ab360363f2d3

📥 Commits

Reviewing files that changed from the base of the PR and between 8fb4f08 and 68bcce3.

📒 Files selected for processing (5)
  • internal/cli/app.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/zerogit/zerogit.go
  • internal/zerogit/zerogit_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/cli/app.go
  • internal/cli/workflows.go

@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed a subtest for CodeRabbit's coverage nitpick: TestPushBranchesToRemote/FailsWhenDefaultBranchCannotBeVerified drives the remote lookup and the local refs/remotes/origin/HEAD record to fail and asserts Push itself refuses with the use --yes guidance, exercising the new fail-closed wrapping directly rather than only through IsDefaultBranch.

Vasanthdev2004
Vasanthdev2004 previously approved these changes Jul 16, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The feature is well-gated branch creation skips on --yes/--dry-run, and the LLM naming path is opt-in via --auto with the diff redacted before it leaves the machine and I like that you hardened isDefaultBranch to fail closed instead of silently downgrading to the main/master name heuristic when the remote lookup times out. Two small things worth a glance: isDefaultBranch now short-circuits on the literal names main/master before consulting the remote, so a repo whose real default is trunk would treat a local feature branch named main as the default (safe direction it only blocks a push, never permits one but slightly surprising); and if the LLM slug generation fails after printing "Generating branch name using LLM..." it silently falls back to the deterministic slug with no follow-up message, which could confuse a user waiting on the LLM. Neither blocks merge.

@euxaristia
euxaristia requested a review from jatmn July 16, 2026 15:21

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Keep an unreachable remote from clearing the default-branch guard
    internal/zerogit/zerogit.go:639-646
    A local refs/remotes/<remote>/HEAD is a cache, not evidence that a differently named branch is safe. If a server renames its default from main to trunk, the local record remains origin/HEAD -> origin/main, and ls-remote then times out or fails, pushing trunk returns false here. Both the preflight and Push repeat that result, so the command pushes the newly protected branch without --yes, despite the intended fail-closed behavior. Only use a cached match to block a branch; when it does not match after live verification fails, return the unknown-default error.

  • [P1] Honor --diff-bytes before sending a branch-name prompt to the provider
    internal/cli/workflows.go:870,928,1057,1072-1079,1115-1121
    changes push/pr accept and document --diff-bytes, but neither call threads options.maxDiffBytes into ensureFeatureBranch, which invokes Inspect with only Cwd. With --auto, the resulting unbounded summary.Diff is embedded in the provider request. A user who supplies a cap to limit proprietary source sent for LLM naming therefore uploads the complete diff; pass the option through to InspectOptions just as the commit path does.

  • [P2] Refuse an auto-branch push when there is no commit to publish
    internal/cli/workflows.go:1057-1096
    The new path branches solely from working-tree status and never establishes that HEAD is ahead of the selected default branch. On a clean, up-to-date default branch it creates and pushes user/<HEAD-subject> at exactly the default tip; with only uncommitted edits it names a branch from those edits but pushes the unchanged HEAD, leaving the edits local. changes pr then leaves that remote branch behind before GitHub rejects the empty comparison. Check that there is a publishable commit range (and report no changes otherwise) before creating or pushing the branch.

  • [P2] Do not treat an LLM preamble as the generated branch slug
    internal/cli/workflows.go:1144-1155
    The helper claims to tolerate non-compliant model responses but returns the first non-empty line verbatim. A common reply such as Here is a suggested branch name:\nadd-login-page creates user/here-is-a-suggested-branch-name; a fenced reply begins with ``` and falls back silently. Parse a valid slug line or strip the supported wrappers before slugifying, and cover preamble and fenced responses.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed fixes for the review findings:

  • Branch generation no longer treats an existing name collision as safe to reuse; it now fails visibly instead of checking out an unrelated branch.
  • The auto-branch path resolves and uses the actual target remote (not always origin) for both the default-branch check and the push.
  • Default-branch lookup now fails closed on a timeout instead of assuming main/master.
  • --diff-bytes is now honored before any diff is sent to a provider for LLM branch naming, and that upload requires explicit opt-in rather than happening on every push/PR.
  • A stale cached remote HEAD can no longer clear the default-branch push guard.
  • Auto-branch push now checks there's an actual commit to publish before creating/pushing a branch.
  • Branch-name parsing now handles LLM replies with preamble text or a fenced code block instead of taking the first line verbatim.

@euxaristia
euxaristia force-pushed the euxaristia/auto-branch-naming-for-changes-workflow branch from 5b0131b to a079996 Compare July 17, 2026 05:32

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Terminate options before passing the remote to ls-remote
    internal/zerogit/zerogit.go:630
    remote can come from --remote=<value> or branch configuration, but it is inserted before the HEAD positional argument without --. A value such as --upload-pack=/bin/echo is parsed by Git as an option; git ls-remote --symref --upload-pack=/bin/echo HEAD invokes that program (and fails with its output as a protocol error). Put -- before the remote and add a dash-prefixed-remote regression test so this preflight cannot turn a remote value into Git options.

  • [P1] Avoid colliding with branches that exist only on the target remote
    internal/zerogit/zerogit.go:756
    The suffix loop probes only refs/heads/<name> locally, then ensureFeatureBranch pushes the generated name to the resolved remote. A pre-existing remote-only <user>/<slug> (for example an old/open PR whose tip is already in main, or a branch absent after pruning) is not detected; git push -u can fast-forward it and silently append the new work to that unrelated remote branch/PR. Check the chosen names against the target remote as well, or fail before creating/pushing, and cover a remote-only collision.

  • [P1] Permit a first feature-branch push to an empty remote
    internal/zerogit/zerogit.go:640
    An empty newly created remote has neither a HEAD symref from ls-remote nor a local refs/remotes/<remote>/HEAD. After the new flow creates a non-default feature branch, Push therefore returns “default branch … unknown” and requires --yes; git remote set-head --auto cannot repair an empty remote. This regresses the first push of a safe non-default branch and conflicts with the preflight’s stated legitimate-first-push path. Distinguish an unborn remote (or otherwise allow this non-default first push) while keeping the default-branch guard fail-closed.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This round landed well — the suffix-based collision handling, threading the resolved remote through the default-branch check and the push, gating the LLM naming behind --auto, and the preamble/fence slug parsing are all what I wanted to see, and CI is green across the board. But I'm with jatmn on his latest pass, so holding approval. The one I care most about: a brand-new empty remote is now a dead end — ls-remote returns no symref, there's no local refs/remotes//HEAD, and the error's own suggested fix (git remote set-head --auto) can't work on an empty remote, so the very first push of a fresh repo needs --yes. That's the exact dead-end UX this PR exists to remove; please carve out the unborn-remote case while keeping the guard fail-closed otherwise (zerogit.go ~640-656). Second, the suffix loop only probes local refs/heads, so a stale same-name branch that exists only on the target remote (an old merged PR branch, say) gets silently fast-forwarded by push -u and inherits the new commits — probe the remote too, or fail before pushing (zerogit.go ~756). The missing -- before the remote in ls-remote predates this PR, but you rewrote that function and already did it right in Push's args, so add it while you're in there. My earlier two notes (the main/master short-circuit and the quiet LLM fallback) still don't gate.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 18, 2026
@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed aabd6f2 for the three open items: (1) isDefaultBranch terminates options with -- before the remote (dash-prefixed-remote regression test included); (2) an unborn remote — ls-remote succeeds with zero refs — now counts as proof there is no protected default, so the first feature-branch push of a fresh repo is no longer a --yes dead end, while every failure path stays fail-closed and main/master stay guarded by the name heuristic; (3) CreateBranch probes the target remote's heads once (bounded, with --) so a remote-only stale branch counts as taken in the suffix loop, and an unreachable remote fails visibly before anything is created or pushed.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Do not treat a missing remote HEAD symref as proof that the remote is empty
    internal/zerogit/zerogit.go:642
    git ls-remote --symref <remote> HEAD also returns no output when a non-empty remote has a dangling or missing HEAD symref. In that state this returns false, nil, which clears the new fail-closed default-branch guard for branches such as trunk; Push/ensureFeatureBranch can then publish without --yes. Confirm that the remote has no heads before allowing the unborn-repository exception, or keep the default branch unknown and require an explicit override.

  • [P1] Refuse when the working tree cannot be represented by the branch being pushed
    internal/cli/workflows.go:1069
    This only checks whether HEAD is ahead and then names a branch from the working-tree snapshot, but CreateBranch and Push publish commits only. With an ahead commit plus additional unstaged edits, the command creates a PR named after the edits while omitting them; if origin/main is absent locally, the ignored CommitsAhead error permits the same empty-branch result with only uncommitted changes. Require a clean working tree (or explicitly commit/stage it) and fail when the publishable commit range cannot be determined, rather than silently creating a partial or empty PR.

  • [P2] Make the remote branch collision check atomic with the push
    internal/zerogit/zerogit.go:784
    The remote-head snapshot is taken before checkout, while the ordinary git push -u occurs later. Another client can create the same generated name in that window; when its ref is at the default tip, this push fast-forwards it and silently appends this work to the other branch/PR. Use a push-time “destination must not exist” condition (or retry a fresh suffix after rejection) so the collision protection cannot be bypassed by a concurrent creator.

  • [P2] Do not make ordinary feature-branch pushes depend on a fixed five-second HEAD lookup
    internal/zerogit/zerogit.go:628
    Every non-main/master Push now gives ls-remote five seconds. A reachable SSH/VPN remote whose handshake or credential negotiation exceeds that limit falls through to the fail-closed error and requires --yes before the normal push can even run; the same cap also blocks the new collision probe. Honor a caller/user deadline or otherwise avoid turning a slow, established remote into a default-branch-verification failure.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 19, 2026
@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed 130b986. This addresses the latest review, all four points.

Fixed:

  • A non-empty remote with a dangling or missing HEAD symref produced the same empty ls-remote --symref output as a genuinely unborn remote, wrongly granting the first-push exception. Added an ls-remote --heads confirmation before granting it.
  • Branch naming (including --auto LLM naming) used a working-tree status snapshot, which could describe uncommitted edits a commit-only push would never publish. Now resolves against the actual remote branch being pushed to.
  • CreateBranch's remote-collision probe and the later push weren't atomic, so a concurrent creator of the same generated name could get silently fast-forwarded. Added a force-with-lease guard requiring the branch not already exist remotely.
  • The 5s ls-remote timeout added for ensureFeatureBranch had ended up inside the shared isDefaultBranch helper, so it was also capping Push's own separate, previously unbounded check. Scoped the timeout to the intended call site only.

Nothing left open, all four points from the latest review are addressed.

Vasanthdev2004
Vasanthdev2004 previously approved these changes Jul 19, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on the current head. All three of my asks are resolved. The unborn/empty-remote case is carved out and now confirmed with a second ls-remote --heads probe before the fail-closed guard is cleared, remote-only collisions are detected and made race-safe with a zero-value force-with-lease, and the missing -- before the remote in the ls-remote preflight is in. It also picked up jatmn's later findings (the HEAD-symref-as-empty case and the unrepresentable-working-tree one).

CI is green and CodeRabbit approved on this head. My two remaining notes (a repo whose real default is "trunk" being treated as a feature branch, and the message left dangling if LLM slug generation fails) are non-gating. Approving.

@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed 521d4ba (on top of 130b986) for the remaining item from the latest review.

From 130b986 (already on this head when the review was filed against aabd6f2):

  • Missing remote HEAD symref is no longer treated as an unborn remote; an empty ls-remote --heads confirmation is required before clearing the fail-closed guard.
  • Branch naming uses the committed range against the resolved remote branch (BaseRef), not a working-tree snapshot.
  • Concurrent remote-name races are closed with --force-with-lease=<branch>: on the push that created the branch.
  • The 5s ls-remote timeout is scoped to IsDefaultBranch / ensureFeatureBranch only; Push's own default-branch check is unbounded again.

From 521d4ba (this push):

  • ensureFeatureBranch now refuses a dirty working tree before any branch is created, so an ahead commit plus uncommitted edits cannot produce a PR that omits those edits.
  • A missing/unresolvable ahead count (never-fetched remote-tracking ref) fails closed with a fetch guidance message instead of guessing there is something to publish.

Covered by TestEnsureFeatureBranchRefusesDirtyWorkingTree and the updated TestEnsureFeatureBranchFailsWhenAheadCountUnknown. go test ./internal/zerogit/... ./internal/cli/... is green.

@euxaristia
euxaristia force-pushed the euxaristia/auto-branch-naming-for-changes-workflow branch from 521d4ba to ef8e072 Compare July 20, 2026 11:07
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed the latest review findings:

[P1] Missing remote HEAD symref is not proof the remote is empty
isDefaultBranch only grants the unborn-remote exception after a second ls-remote --heads confirms the remote has no branches. A non-empty remote with a dangling/missing HEAD fails closed.

[P1] Refuse when the working tree cannot be represented by the branch being pushed
ensureFeatureBranch now requires a clean working tree, fails when the ahead count against the remote default cannot be determined, and names the branch from the committed range (BaseRef) rather than a working-tree snapshot.

[P2] Remote branch collision check atomic with push
Newly created feature branches push with --force-with-lease=<branch>: so a concurrent creator of the same name is rejected instead of silently fast-forwarded.

[P2] Ordinary feature-branch pushes no longer depend on a fixed 5s HEAD lookup
Removed the hard 5s timeout from IsDefaultBranch and CreateBranch. Both honor the caller context (same as Push), so a slow but reachable remote is not turned into a default-branch-verification failure. Callers that need a bound pass a deadline.

Also rebased onto current main.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Keep the nonexistence lease on a retry after a collision
    internal/cli/workflows.go:1068
    When the initial auto-branch push loses the race with another creator, the explicit --force-with-lease=<branch>: correctly rejects it but leaves the local generated branch checked out. Retrying takes this non-default early return and clears RequireNewRemoteBranch, so the ordinary push can fast-forward the concurrently-created branch (and append to its PR) when it started at the same default tip. Preserve the lease or recheck/disambiguate the remote branch on this retry path.

  • [P2] Resolve main/master against the selected remote before treating it as protected
    internal/zerogit/zerogit.go:632
    This shortcut now refuses or auto-branches any branch named main/master without consulting the requested remote. A repository whose remote default is trunk can legitimately have a non-default local main (including one tracking origin/trunk); the prior implementation accepted the live symref result, while this path either creates an unnecessary branch or subsequently fails looking up the nonexistent origin/main. Apply the conventional-name fallback only when the remote default cannot be determined.

  • [P2] Validate publishability against the live default, not a stale tracking ref
    internal/cli/workflows.go:1092
    The preflight has already contacted the remote, but it decides whether there is work to publish from remote/currentBranch in the local ref database. If origin/main is stale at A while the remote has already advanced to local HEAD C, this count is positive and the command creates/pushes a feature branch whose PR has an empty comparison. Fetch or otherwise compare with the advertised default ref before creating the branch and deriving its diff.

euxaristia and others added 16 commits July 22, 2026 12:25
…fault branch

zero changes push/pr refused to push straight to the default branch but
never offered an alternative, unlike other agent tooling that names and
checks out a feature branch automatically. Add CreateBranch, IsDefaultBranch,
CurrentGitUser, and branch-name slugify/build helpers to zerogit, and wire
push/pr to auto-create a "<user>/<slug>" branch (slug from an LLM when a
provider is configured, otherwise a deterministic fallback from the diff)
whenever the current branch is the default one and --yes/--dry-run weren't
passed.
… existing branches

Address CodeRabbit's review on the auto-branch-naming PR:

- generateAutoBranchSlug now takes the first non-empty, quote-trimmed
  line of the model's response before slugifying, instead of folding
  any preamble or wrapping quotes into the branch name.
- isDefaultBranch's ls-remote lookup is now bounded by a 5s timeout so
  a slow or unreachable remote can't stall push/pr; falls back to the
  local main/master heuristic as before.
- CreateBranch checks whether the target branch already exists locally
  and checks it out instead of failing checkout -b on the collision.

Also add regression coverage: CurrentGitUser's OS-username fallback
tier, CreateBranch's existing-branch and generic checkout-failure
paths, a CLI-level test for changes pr (previously untested) mirroring
the existing changes push coverage, and a messy-LLM-response case for
the slug normalization fix.
…ail-closed, and opt-in for LLM naming

- CreateBranch never checks out an existing branch on a name collision: it
  picks a unique suffixed name off the current HEAD (name-2 .. name-9) and
  fails visibly when the namespace is exhausted, so a stale branch with
  unrelated history can no longer be published while the new commit stays
  behind on the default branch.
- ensureFeatureBranch resolves the remote once (explicit --remote, then the
  original branch's configured upstream, then origin) via IsDefaultBranch,
  which now reports the resolved remote, and push/pr thread that remote into
  Push, so a freshly created branch without tracking configuration no longer
  falls back to origin in fork setups.
- The default-branch check fails closed: main and master count as default
  with no network, an unreachable remote falls back to the local
  refs/remotes/<remote>/HEAD record, and when neither answers the push is
  refused with guidance instead of silently dropping the confirmation guard
  for repositories whose default is trunk or develop.
- LLM branch naming is now opt-in via --auto on push/pr, matching commit
  --auto: a configured provider alone no longer causes the change diff to be
  uploaded during ordinary git-only pushes.
- After the ordinary commit-then-push sequence the tree is clean, so the
  fallback name comes from the HEAD commit subject instead of the constant
  user/changes.
…guard

When the live ls-remote default-branch lookup fails, isDefaultBranch fell
back to the local refs/remotes/<remote>/HEAD cache and returned
branch == cachedName. That cache is only a hint: if the server renamed its
default (main -> trunk) the record still names main, so checking whether
pushing trunk is safe returned false and the newly protected branch was
pushed without --yes.

Trust the cache only to block a push (a positive match proves the branch is
the recorded default); on a mismatch fall through to the existing
fail-closed unknown-default error instead of treating the branch as
unprotected.
… naming

changes push/pr accept and document --diff-bytes, but ensureFeatureBranch
inspected the working tree with only Cwd set. With --auto the resulting
unbounded diff was embedded in the branch-name request to the provider, so a
user who capped the diff to limit how much proprietary source is uploaded
for naming still sent the complete diff.

Thread options.maxDiffBytes through ensureFeatureBranch into InspectOptions,
matching how the commit path already bounds the diff.
The auto-branch path decided to branch solely from working-tree status and
never checked that HEAD was ahead of the default branch. On a clean,
up-to-date default branch it would create and push user/<subject> at the
exact default tip; with only uncommitted edits it named a branch from those
edits but pushed the unchanged HEAD, leaving the edits local, and changes pr
then abandoned the empty branch before the host rejected the comparison.

Add zerogit.CommitsAhead and consult it before branching: when HEAD is
provably not ahead of <remote>/<default>, report "no changes to publish"
instead of creating the branch. An indeterminate count (for example a
remote-tracking ref that was never fetched) is treated as "cannot tell" so a
legitimate first push is not blocked.
The slug helper returned the first non-empty line verbatim, so a reply like
"Here is a suggested branch name:\nadd-login-page" produced
user/here-is-a-suggested-branch-name, and a reply wrapped in a code fence
slugified the ``` line to nothing and silently fell back to the local name.

Drop Markdown code-fence lines and prefer a line that already reads as a
kebab-case slug, falling back to the first quoted/plain line only when no
slug-shaped line exists. Cover preamble and fenced responses with tests.
Three review findings:

- isDefaultBranch passed the remote to ls-remote before the HEAD
  positional with no --, so a remote value shaped like an option
  (--upload-pack=/bin/echo) was parsed as one. Terminate options with --
  and cover a dash-prefixed remote.

- An unborn remote (freshly created, zero refs) answered ls-remote with
  empty output, fell through to the fail-closed unknown-default error,
  and made the very first feature-branch push a --yes dead end that
  git remote set-head --auto cannot repair. ls-remote succeeding with no
  refs now counts as proof there is no protected default; main/master
  stay guarded by the name heuristic and every failure path stays
  fail-closed.

- CreateBranch probed only local refs/heads for collisions, so a branch
  existing only on the target remote (an old merged-PR branch) was
  silently fast-forwarded by the later push -u. The target remote's
  heads are now probed once (bounded, with --) and count as taken;
  an unreachable remote fails visibly before anything is created.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Require a confirmed unborn HEAD, not just an empty ls-remote --symref,
before granting the first-push exception, since a dangling remote
HEAD looked the same. Resolve branch naming and ahead-count checks
against the remote branch instead of a working-tree snapshot, which
could describe uncommitted edits a commit-only push would never
publish. Guard concurrent branch creation with a force-with-lease
push. Scope the ls-remote timeout to the default-branch preflight
only, so it no longer caps Push's own unrelated check.
Refuse ensureFeatureBranch when the working tree is dirty or the
ahead count against the remote default cannot be determined, so a
default-branch push cannot leave uncommitted edits behind or publish
an empty comparison under a guessed name.
IsDefaultBranch and CreateBranch applied a hard 5s bound on ls-remote,
so ordinary feature-branch pushes and the collision probe failed on slow
but reachable remotes (SSH/VPN) before Push could run. Honor the
caller's context instead; callers that need a bound pass a deadline.
…unborn remote

jatmn flagged that the confirmed-unborn-remote exception this PR added to
IsDefaultBranch never actually helps the main/master case it was written
for: when the local branch is named main, isDefaultBranch returns true from
the name heuristic before it ever consults the remote, so
ensureFeatureBranch still unconditionally calls commitsAhead against
<remote>/<branch>. On a genuinely empty remote that ref cannot exist, so
`zero changes push`/`pr` from a local default branch against a brand-new
empty remote still dead-ended with "cannot determine whether HEAD is
ahead of origin/main" - the exact bug this PR set out to fix.

Tracing the flow further, the same problem also hit the diff-base Inspect
call right after: it uses the same nonexistent <remote>/<branch> ref, so
bypassing only commitsAhead would have left the dead end one line later.

ensureFeatureBranch now probes the new zerogit.IsUnbornRemote (ls-remote
--heads on the remote) whenever commitsAhead fails, and only bypasses both
checks when that probe positively confirms the remote has no refs at all.
An unreachable remote, or one that answers with refs, still fails closed
exactly as before - the fail-closed "unknown default branch" behavior is
unchanged and covered by TestEnsureFeatureBranchFailsWhenAheadCountUnknown
and the new TestEnsureFeatureBranchFailsWhenUnbornCheckErrors.

TestEnsureFeatureBranchCreatesBranchOnConfirmedUnbornRemote is the
regression test: confirmed failing (with the exact dead-end error) against
the pre-fix logic, passing after.
isDefaultBranch short-circuited on the literal branch names main/master
before ever consulting the remote. A repository whose remote default is
genuinely something else (e.g. trunk) could still have a local main branch
tracking that remote, and the shortcut wrongly treated it as protected
instead of trusting the live symref result. The conventional-name check
now only applies as a last-resort fallback, after both the live ls-remote
symref lookup and the local remotes/<remote>/HEAD cache fail to answer.
Two related gaps in ensureFeatureBranch's auto-branch preflight:

- When an auto-branch push loses a force-with-lease race against a
  concurrent creator, the generated branch is left checked out locally
  with no successful push behind it. A retry took the "already off the
  default branch" early return and dropped RequireNewRemoteBranch,
  letting the next push silently fast-forward whatever the concurrent
  creator published. The lease now stays required unless the branch
  already has a configured upstream (proof a push already landed).

- The ahead-of-default check compared HEAD against the local
  remote-tracking ref, which is only refreshed at clone or the last
  fetch. A stale ref can under-report how far ahead HEAD is, or say
  zero when the remote has actually moved past it, so ensureFeatureBranch
  now refreshes that tracking ref before trusting it, and fails closed
  if the refresh itself cannot be confirmed.

Adds regression coverage for both: a retry after a lease collision keeps
requiring the lease (and an already-published branch does not), and a
stale tracking ref is refreshed before the ahead check runs (with a
fail-closed case when that refresh cannot be confirmed).
@euxaristia
euxaristia force-pushed the euxaristia/auto-branch-naming-for-changes-workflow branch from 4a3cd0c to b1dc729 Compare July 22, 2026 16:39
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed the three findings from the latest review.

Keep the nonexistence lease on a retry after a collision (workflows.go:1068): when ensureFeatureBranch is called again with the current branch already off the default (the case a retry after a lost force-with-lease race hits), it now checks whether that branch has a configured upstream before deciding whether Push needs RequireNewRemoteBranch. No upstream means no push has landed for it yet, so the lease stays required; an already-published branch (has an upstream) does not need it reapplied. Added zerogit.HasUpstream for this check.

Resolve main/master against the selected remote before treating it as protected (zerogit.go:632): isDefaultBranch no longer short-circuits on the literal names before consulting the remote. It now tries the live ls-remote symref result first, then the local remotes//HEAD cache, and only falls back to the conventional main/master names when neither can answer. A repo whose real default is something else (trunk, say) with a local main tracking it is no longer misidentified.

Validate publishability against the live default, not a stale tracking ref (workflows.go:1092): before checking whether HEAD is ahead of the remote default, ensureFeatureBranch now refreshes the local remote-tracking ref for that branch (a targeted fetch of just that ref) so a merely-cached origin/main can't make a real publishable range look like zero commits ahead. If that refresh can't be confirmed, it fails closed the same way an unresolvable ahead count already did, instead of trusting whatever the stale ref reports.

Tests added: a retry-after-lease-collision case (and its companion, an already-published branch not requiring the lease) in internal/cli/workflow_test.go; a non-default local main tracking a remote whose live default is something else in internal/zerogit/zerogit_test.go; and a stale-tracking-ref-but-remote-has-advanced case plus a fail-closed case when the refresh itself fails, in internal/cli/workflow_test.go.

Rebased onto upstream/main (one unrelated cron fix was ahead) before making these changes. go build, go vet, go test, and gofmt all pass on internal/cli and internal/zerogit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/cli/workflow_test.go (1)

1107-1141: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Diff-bytes cap is only proven to reach Inspect, not the provider request.

The comment justifies the cap by the --auto LLM-upload privacy path ("proprietary source sent for LLM naming"), but this test runs with autoNaming=false, so the provider is never constructed and the truncated diff is never asserted to land in the provider request. The privacy guarantee the comment describes is therefore untested end-to-end; only the cap→Inspect hop is covered.

Consider adding an --auto variant that stubs newProvider and asserts the captured zeroruntime.CompletionRequest carries the capped diff (the mockCommitMsgProvider.req capture at Line 721 already makes this cheap).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` around lines 1107 - 1141, Extend
TestEnsureFeatureBranchThreadsDiffBytesToInspect with an auto-naming path that
enables provider creation, stubs newProvider using the existing
mockCommitMsgProvider request capture, and asserts zeroruntime.CompletionRequest
contains the capped diff. Preserve the existing assertion that
options.MaxDiffBytes reaches inspectChanges, while adding the end-to-end
provider-request verification described by the test comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/cli/workflow_test.go`:
- Around line 1107-1141: Extend TestEnsureFeatureBranchThreadsDiffBytesToInspect
with an auto-naming path that enables provider creation, stubs newProvider using
the existing mockCommitMsgProvider request capture, and asserts
zeroruntime.CompletionRequest contains the capped diff. Preserve the existing
assertion that options.MaxDiffBytes reaches inspectChanges, while adding the
end-to-end provider-request verification described by the test comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 433dc4c3-84e5-49d0-8956-3e40991231f4

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3cd0c and b1dc729.

📒 Files selected for processing (5)
  • internal/cli/app.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/zerogit/zerogit.go
  • internal/zerogit/zerogit_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/cli/app.go
  • internal/cli/workflows.go
  • internal/zerogit/zerogit.go
  • internal/zerogit/zerogit_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 22, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Keep conventional default branches on the auto-branch path for an unborn remote
    internal/zerogit/zerogit.go:641
    A newly created remote makes both ls-remote --symref and --heads return empty, so this returns false before reaching the main/master fallback. Consequently, ensureFeatureBranch takes its non-default early return and changes push publishes local main directly without --yes, rather than creating the promised feature branch. This also makes the confirmed-unborn handling below in ensureFeatureBranch unreachable in production: its regression test stubs isDefaultBranch to true. Preserve the default-branch classification for conventional local defaults while allowing the later unborn-remote logic to handle the missing tracking ref, and cover the real IsDefaultBranchensureFeatureBranch path.

  • [P2] Do not push before attempting changes pr against an unborn remote
    internal/cli/workflows.go:930
    Correcting the default-branch classification above reaches the confirmed-unborn branch path, which pushes a generated branch and then unconditionally invokes gh pr create. An empty GitHub repository has no independent base branch: its first pushed branch becomes the default, so there is no comparison for a pull request and the command fails after leaving a remote branch behind. Detect this unsupported pr case before the push (or bootstrap an explicit base as part of a defined workflow), rather than reporting a failed PR after the side effect.

  • [P2] Do not infer a collision retry from a missing upstream
    internal/cli/workflows.go:1083
    Every non-default branch without a configured upstream is marked RequireNewRemoteBranch, including an ordinary local checkout of an existing remote feature branch (for example, git switch --no-track origin/feature). The subsequent git push --force-with-lease=<branch>: requires the remote branch not to exist and rejects that normal upstream-establishment push as stale. The previous push flow supported it; retain explicit provenance for a generated branch that lost the creation race, or otherwise distinguish it from manually checked-out no-upstream branches.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 23, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Keep the new-branch lease when --force is requested
    internal/zerogit/zerogit.go:604
    Auto-created branches set RequireNewRemoteBranch specifically to prevent a branch created after the preflight probe from being updated. However, Force wins this switch and replaces the explicit --force-with-lease=<branch>: with the generic form. With a stale local refs/remotes/<remote>/<branch> at the default tip, a concurrent creator can publish that branch at the same tip during the probe-to-push window; generic --force-with-lease then accepts and fast-forwards their branch. Preserve the explicit nonexistence lease (or reject this flag combination) whenever RequireNewRemoteBranch is set.

  • [P2] Do not discard the generated-branch marker write failure
    internal/cli/workflows.go:1187
    The retry path retains the nonexistence lease only when IsGeneratedBranch reads the marker written here. If git config branch.<name>.zeroAutoBranch true fails after checkout (for example due to config-lock contention), the initial push can lose its lease race, but the retry sees an unmarked non-default branch and drops RequireNewRemoteBranch; it can then fast-forward the concurrent branch. Return the marker error or retain the lease through another durable mechanism.

  • [P3] Reject an unborn remote before switching branches for changes pr
    internal/cli/workflows.go:930
    On a default branch with a confirmed-unborn remote, ensureFeatureBranch creates and checks out a generated branch before lines 935–939 reject PR creation and tell the user to push the initial default branch. The failed command therefore strands the user on the feature branch; following that instruction now requires manually switching back first. Check the remote before branch creation or restore the original branch on this error path.

…agate marker write errors, check unborn remote before branch switch

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Keep the collision lease when a generated branch is pushed to a different remote
    internal/cli/workflows.go:1087-1092
    The retry path treats any configured upstream as proof that the generated branch has already been published, without checking whether it is the remote selected for this invocation. After an auto-created branch has been pushed to origin, zero changes push --remote upstream drops RequireNewRemoteBranch; if upstream already has the same branch name, the ordinary push can fast-forward that stale branch. Compare the configured upstream remote with the selected remote, or re-probe/assert nonexistence on every different target remote.

  • [P2] Preserve collision protection after the generated-branch marker write fails
    internal/cli/workflows.go:1192-1195
    CreateBranch has already switched to the new branch when MarkGeneratedBranch can fail (for example while .git/config.lock is present). Returning the error leaves that branch checked out but unmarked; on retry it follows the ordinary non-default path and drops RequireNewRemoteBranch. A concurrent creator can then create the same remote ref after the first probe and have the retry fast-forward it. Roll back safely or retain durable/recomputed nonexistence protection across this partial-success state.

  • [P2] Let --yes actually bypass the default-branch preflight for PR creation
    internal/cli/workflows.go:930-932
    changes pr --yes is documented as bypassing the auto-branch step, and both ensureFeatureBranch and Push honor that override. The new unconditional preliminary isDefaultBranch call still fails when the remote HEAD cannot be resolved or the remote is temporarily unavailable, so the command aborts before the allowed push. Skip this verification under --yes, or resolve the destination remote without requiring a default-branch determination.

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.

3 participants