branch rename: Rename uses the new API#14675
Conversation
estib-vega
commented
Jul 8, 2026
- Introduce branch_rename.
- This correctly re-checks out the HEAD if in single branch mode
- This correctly updates the metadata
There was a problem hiding this comment.
Pull request overview
This PR introduces a new branch_rename backend API/command and updates the desktop UI to use it, with accompanying Rust API tests and Playwright coverage to validate correct HEAD/workspace behavior in single-branch mode.
Changes:
- Add
branch_renametobut-api(including JSON transport + SDK types) and expose it viabut-server. - Switch desktop branch rename flows (modal + inline edit) to call the new
branchRenameendpoint with optimistic UI updates. - Add Rust API tests (rename/remove/create/default-target/checkout) and Playwright tests for renaming in single-branch stacks.
Reviewed changes
Copilot reviewed 15 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/but-sdk/src/generated/linear/index.d.ts | Adds BranchRenameResult type to generated SDK typings. |
| packages/but-sdk/src/generated/graph/index.d.ts | Adds BranchRenameResult type to generated SDK typings. |
| e2e/playwright/tests/singleBranch/commitActions.spec.ts | Adds Playwright coverage for renaming checked-out and stacked branches (header + inline). |
| crates/but-server/src/lib.rs | Registers the new /branch_rename route and branch_rename command dispatch. |
| crates/but-api/tests/api/support.rs | Adds shared repo/test helpers used by new branch API tests. |
| crates/but-api/tests/api/main.rs | Wires new branch test modules into the test suite. |
| crates/but-api/tests/api/branch_rename.rs | New API tests validating rename semantics (HEAD movement, normalization, metadata, remote-tracking ref untouched). |
| crates/but-api/tests/api/branch_remove.rs | New API tests validating remove semantics and HEAD movement for empty tips. |
| crates/but-api/tests/api/branch_default_target.rs | New API tests validating default-target metadata persistence and merge-base logic. |
| crates/but-api/tests/api/branch_create.rs | New API tests validating create-above/create-below checkout behavior and ordering. |
| crates/but-api/tests/api/branch_checkout.rs | Expands checkout tests to include new-checkout behavior and ref validation. |
| crates/but-api/src/branch.rs | Implements branch_rename (core behavior + JSON mapping) and removes inlined tests moved to dedicated test modules. |
| apps/desktop/src/lib/testing/mockStackService.ts | Adds branchRename to stack service mocks. |
| apps/desktop/src/lib/stacks/stackService.svelte.ts | Adds branchRename mutation wrapper with optimistic UI side effects. |
| apps/desktop/src/lib/stacks/stackEndpoints.ts | Adds the branchRename backend endpoint/mutation definition. |
| apps/desktop/src/components/branch/BranchRenameModal.svelte | Switches modal rename flow to call the new branchRename endpoint. |
| apps/desktop/src/components/branch/BranchCard.svelte | Switches inline rename to branchRename and aligns loading/error UI state. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d7c96fe0e9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// Whether `a` and `b` are in a directory/file prefix relationship, i.e. one full ref name is a | ||
| /// path-component prefix of the other (`refs/heads/foo` vs `refs/heads/foo/bar`). Such refs cannot | ||
| /// coexist on disk, so a rename between them needs a single atomic delete-and-create. A shared | ||
| /// prefix that is not on a `/` boundary (`foo` vs `foobar`) is not prefix-related. |
ae8d097 to
a7dd4e4
Compare
| /// Return the worktree directories whose `HEAD` points to `rn` (directly or indirectly), | ||
| /// or `None` if no worktree does. Unlike [`delete_reference()`](Self::delete_reference) this | ||
| /// performs no mutation, so it can be used to check the precondition up front. | ||
| pub fn worktree_dirs_with_ref(&self, rn: &gix::Reference) -> Option<&[PathBuf]> { | ||
| self.worktrees_by_ref | ||
| .get(&rn.inner.name) | ||
| .map(|paths| paths.as_slice()) | ||
| } |
There was a problem hiding this comment.
This is the only thing that falls under the code owners authority
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a7dd4e4865
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| // Move all metadata (per-branch blob + branch-order entry) to the new name. | ||
| meta.rename(ref_name.as_ref(), new_ref.as_ref())?; |
There was a problem hiding this comment.
Avoid applying ref changes before metadata can rename
When the destination exists only in GitButler metadata (for example a stale branch_order entry left after an external branch deletion), the earlier repo.try_find_reference(new_ref) check passes, but meta.rename() can still reject the destination. At this point the code has already created new_ref, deleted ref_name, and possibly moved HEAD, so the API returns an error while leaving the repository renamed and metadata still keyed by the old branch. Preflight the metadata conflict or make the ref/metadata steps rollback together before reaching this call.
Useful? React with 👍 / 👎.
a7dd4e4 to
a9f4d49
Compare
Use the new branch rename API in the desktop application. Add E2E tests for it all
a9f4d49 to
13be939
Compare
| // EDIT | ||
| case "UpdateBranchName": | ||
| return { | ||
| text: `Renamed branch "${trailer("before")}" to "${trailer("after")}"`, | ||
| text: `Renamed branch "${trailer("previous_name")}" to "${trailer("name")}"`, | ||
| icon: "edit", | ||
| }; |
| old_reference | ||
| .delete() | ||
| .with_context(|| format!("Could not delete branch '{}'", ref_name.as_bstr()))?; | ||
| repo.reference( | ||
| new_ref.as_ref(), | ||
| target_id, | ||
| PreviousValue::MustNotExist, | ||
| "rename branch", | ||
| ) | ||
| .with_context(|| format!("Could not create branch '{}'", new_ref.as_bstr()))?; |
| // Renaming onto the same name is a no-op that still returns the current view. | ||
| if ref_name.as_ref() == new_ref.as_ref() { | ||
| let mut meta = ctx.meta()?; | ||
| let (repo, ws, _db) = ctx.workspace_mut_and_db_with_perm(perm)?; | ||
| let workspace = WorkspaceState::from_workspace(&ws, &mut meta, &repo, BTreeMap::new())?; | ||
| return Ok(BranchRenameResult { workspace, new_ref }); | ||
| } |
| case "UpdateBranchName": | ||
| return { | ||
| text: `Renamed branch "${trailer("before")}" to "${trailer("after")}"`, | ||
| text: `Renamed branch "${trailer("previous_name")}" to "${trailer("name")}"`, |