Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ These features exist specifically to make ez useable by AI agents:
| 0.2.24 | `ez sync`/`ez list` PR-status lookup uses one GraphQL request with aliased fields per branch instead of paginating every PR in the repo (122s → 1.8s on a 10k-PR repo); add local git-remote URL parser to skip the `gh repo view` round-trip when deriving owner/repo; `ez adopt` still uses the global PR scan because it needs the full PR graph. Adds `ez track [branch] [--parent <name>]` to bring a raw-git branch under ez management without rebasing — parent defaults to the closest tracked ancestor by merge-base, else trunk; updates `BranchNotInStack` error to hint at `ez track`. |
| 0.2.25 | `ez adopt` scopes to local branches by default — one GraphQL call for local-branch PRs instead of paginating every PR in the repo (122s → 1.4s adopting 12 PRs in a 10k-PR repo). `--branches <names>` and `--pr <N>` walk the ancestor chain on demand, fetching only the parents that show up in the chain (bounded by stack depth, not repo size). Adds `github::get_pr_by_number` for single-PR GraphQL lookup. Local PRs whose base isn't local-with-PR or trunk are warned about and skipped — users get a `Run \`ez adopt --pr <N>\`` hint to walk the full chain via remote PR ancestors when needed. |
| 0.2.27 | Restack branches in their worktree instead of skipping: `ez sync`, `ez restack`, and all auto-restack paths run `git -C <worktree>` when a branch is checked out elsewhere (including external worktrees like Superconductor). |
| 0.2.28 | `ez create -m` commits on the new branch without advancing the parent (stashes uncommitted state and transfers it into the new worktree; rejects `--no-worktree`). Fix stale-`parent_head` auto-restack bug: `ez commit`/`ez amend`/`ez move` now cascade the restack across the **full descendant subtree** (`StackState::descendants_topo` + shared `restack::cascade_restack`) instead of only direct children, which left grandchildren detached from the stack until a manual `ez restack`. |

---

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ez-stack"
version = "0.2.27"
version = "0.2.28"
edition = "2024"
rust-version = "1.85"
description = "A CLI tool for managing stacked PRs with GitHub"
Expand Down
39 changes: 6 additions & 33 deletions src/cmd/amend.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::{Result, bail};

use crate::cmd::rebase_conflict;
use crate::cmd::restack;
use crate::error::EzError;
use crate::git;
use crate::stack::StackState;
Expand Down Expand Up @@ -59,41 +59,14 @@ pub fn run(message: Option<&str>, all: bool) -> Result<()> {
"deletions": del,
}));

// Auto-restack children of the current branch.
let current_head = after;
let children = state.children_of(&current);

// Auto-restack the whole subtree below the amended branch — not just direct
// children, which would leave grandchildren detached from the stack.
let current_root = git::repo_root()?;

for child_name in &children {
let old_parent_head = state.get_branch(child_name)?.parent_head.clone();

let sp = ui::spinner(&format!("Restacking `{child_name}`..."));
let outcome = git::rebase_onto_for_branch(
&current_head,
&old_parent_head,
child_name,
&current_root,
)?;
sp.finish_and_clear();

match outcome {
git::RebaseOutcome::RebasingComplete => {
let child = state.get_branch_mut(child_name)?;
child.parent_head = current_head.clone();
ui::info(&format!("Restacked `{child_name}`"));
}
git::RebaseOutcome::Conflict(conflict) => {
git::checkout(&current)?;
state.save()?;
rebase_conflict::report("amend", child_name, &current, &conflict, "ez restack");
bail!(EzError::RebaseConflict(child_name.clone()));
}
}
}
let restacked =
restack::cascade_restack(&mut state, &current, &current_root, &current, "amend")?;

// Return to the original branch after restacking (only if we may have moved).
if !children.is_empty() {
if restacked > 0 {
git::checkout(&current)?;
}

Expand Down
41 changes: 9 additions & 32 deletions src/cmd/commit.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use anyhow::{Result, bail};
use anyhow::Result;

use crate::cmd::mutation_guard;
use crate::cmd::mutation_guard::StageMode;
use crate::cmd::rebase_conflict;
use crate::error::EzError;
use crate::cmd::restack;
use crate::git;
use crate::stack::StackState;
use crate::ui;
Expand Down Expand Up @@ -63,43 +62,21 @@ pub fn run(
"out_of_scope_files": outcome.scope.out_of_scope_files,
}));

// Auto-restack children so they stay on top of the new HEAD.
let new_head = after;
let children = state.children_of(&current);

// Auto-restack the whole subtree so every descendant stays on top of the new
// HEAD — not just direct children (which would leave grandchildren detached).
let current_root = git::repo_root()?;
let mut restacked_count = 0;

for child in &children {
let meta = state.get_branch(child)?;
let old_base = meta.parent_head.clone();

ui::info(&format!("Restacking `{child}`..."));
match git::rebase_onto_for_branch(&new_head, &old_base, child, &current_root)? {
git::RebaseOutcome::RebasingComplete => {}
git::RebaseOutcome::Conflict(conflict) => {
// Save progress so the user can fix conflicts and continue with `ez restack`.
state.save()?;
git::checkout(&current)?;
rebase_conflict::report("commit", child, &current, &conflict, "ez restack");
bail!(EzError::RebaseConflict(child.clone()));
}
}
let restacked_count =
restack::cascade_restack(&mut state, &current, &current_root, &current, "commit")?;

let meta = state.get_branch_mut(child)?;
meta.parent_head = new_head.clone();
restacked_count += 1;
}

// After restacking we may be on a child branch; return to the original.
if !children.is_empty() {
// Restacking may have left us on a descendant branch; return to the original.
if restacked_count > 0 {
git::checkout(&current)?;
}

state.save()?;

if restacked_count > 0 {
ui::info(&format!("Restacked {restacked_count} child branch(es)"));
ui::info(&format!("Restacked {restacked_count} branch(es)"));
}

Ok(())
Expand Down
Loading
Loading