Skip to content
44 changes: 42 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ Examples:
ez push -am \"feat: add auth\"
ez push -Am \"feat: add auth and new snapshots\"
ez push --repo owner/repo
ez push --remote fork --repo owner/repo")]
ez push --remote fork --repo owner/repo
ez push --repoint")]
Push {
/// Create a draft PR
#[arg(long, conflicts_with = "no_pr")]
Expand Down Expand Up @@ -198,6 +199,10 @@ Examples:
/// Git remote to push to (overrides config), stored per-branch on first use
#[arg(long)]
remote: Option<String>,

/// Force close-and-recreate the PR in the correct repo (cross-fork repointing)
#[arg(long, conflicts_with = "no_pr")]
repoint: bool,
},

/// Push and create/update PRs for the entire stack
Expand Down Expand Up @@ -247,7 +252,8 @@ Examples:
ez sync
ez sync --autostash
ez sync --dry-run
ez sync --force")]
ez sync --force
ez sync --submit")]
Sync {
/// Show what sync would do without making changes
#[arg(long)]
Expand All @@ -260,6 +266,10 @@ Examples:
/// Force-remove worktrees and branches even if they have uncommitted changes
#[arg(long)]
force: bool,

/// After sync, push and update PRs for the entire stack (equivalent to ez submit)
#[arg(long)]
submit: bool,
},

/// Fetch trunk, refresh it locally, and rebase stale branches onto their latest parent tips
Expand Down Expand Up @@ -1105,4 +1115,34 @@ mod tests {
_ => panic!("expected submit command"),
}
}

#[test]
fn parses_push_repoint_flag() {
let cli = Cli::try_parse_from(["ez", "push", "--repoint"])
.expect("parse push --repoint");
match cli.command {
Commands::Push { repoint, .. } => {
assert!(repoint);
}
_ => panic!("expected push command"),
}
}

#[test]
fn push_repoint_conflicts_with_no_pr() {
let result = Cli::try_parse_from(["ez", "push", "--repoint", "--no-pr"]);
assert!(result.is_err(), "--repoint and --no-pr should conflict");
}

#[test]
fn parses_sync_submit_flag() {
let cli = Cli::try_parse_from(["ez", "sync", "--submit"])
.expect("parse sync --submit");
match cli.command {
Commands::Sync { submit, .. } => {
assert!(submit);
}
_ => panic!("expected sync command"),
}
}
}
3 changes: 3 additions & 0 deletions src/cmd/checkout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ mod tests {
push_remote: None,
scope: None,
scope_mode: None,
repoint: None,
target_pr_repo: None,
},
);
StackState {
Expand All @@ -224,6 +226,7 @@ mod tests {
draft: None,
no_pr: None,
rerere: None,
repoint: None,
branches,
}
}
Expand Down
12 changes: 11 additions & 1 deletion src/cmd/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const KNOWN_KEYS: &[(&str, &str)] = &[
("draft", "Default new PRs to draft (true/false)"),
("no_pr", "Default push to skip PR creation (true/false)"),
("rerere", "Enable git rerere for conflict recording (true/false)"),
("repoint", "Enable cross-repo PR repointing on sync/push (true/false, default true)"),
];

/// Known branch attribute keys.
Expand All @@ -29,10 +30,11 @@ const BRANCH_KEYS: &[(&str, &str)] = &[
("parent", "Parent branch in the stack"),
("scope", "File scope patterns (comma-separated)"),
("scope_mode", "Scope enforcement mode (warn/strict)"),
("repoint", "Enable cross-repo PR repointing for this branch (true/false, default true)"),
];

/// Global keys that accept only boolean values.
const BOOL_KEYS: &[&str] = &["draft", "no_pr", "rerere"];
const BOOL_KEYS: &[&str] = &["draft", "no_pr", "rerere", "repoint"];

fn is_known_global_key(key: &str) -> bool {
KNOWN_KEYS.iter().any(|(k, _)| *k == key)
Expand Down Expand Up @@ -347,6 +349,7 @@ fn get_global_value(state: &StackState, key: &str) -> Option<String> {
"draft" => state.draft.map(|v| v.to_string()),
"no_pr" => state.no_pr.map(|v| v.to_string()),
"rerere" => state.rerere.map(|v| v.to_string()),
"repoint" => state.repoint.map(|v| v.to_string()),
_ => None,
}
}
Expand Down Expand Up @@ -382,6 +385,9 @@ fn set_global_value(state: &mut StackState, key: &str, value: &str) -> Result<()
enable_rerere();
}
}
"repoint" => {
state.repoint = Some(parse_bool(value)?);
}
_ => {
bail!(EzError::UserMessage(format!("unknown config key `{key}`")));
}
Expand Down Expand Up @@ -411,6 +417,7 @@ fn get_branch_value(meta: &crate::stack::BranchMeta, key: &str) -> Option<String
crate::stack::ScopeMode::Warn => "warn".to_string(),
crate::stack::ScopeMode::Strict => "strict".to_string(),
}),
"repoint" => meta.repoint.map(|v| v.to_string()),
// Also allow reading per-branch `remote` as alias for push_remote
"remote" => meta.push_remote.clone(),
_ => None,
Expand Down Expand Up @@ -480,6 +487,9 @@ fn set_branch_value(
};
state.get_branch_mut(branch)?.scope_mode = Some(mode);
}
"repoint" => {
state.get_branch_mut(branch)?.repoint = Some(parse_bool(value)?);
}
_ => {
bail!(EzError::UserMessage(format!(
"unknown branch attribute `{key}`\n → Known attrs: {}",
Expand Down
3 changes: 3 additions & 0 deletions src/cmd/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ mod tests {
push_remote: None,
scope: None,
scope_mode: None,
repoint: None,
target_pr_repo: None,
},
);
StackState {
Expand All @@ -253,6 +255,7 @@ mod tests {
draft: None,
no_pr: None,
rerere: None,
repoint: None,
branches,
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/cmd/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ mod tests {
push_remote: None,
scope: None,
scope_mode: None,
repoint: None,
target_pr_repo: None,
},
);
branches.insert(
Expand All @@ -225,6 +227,8 @@ mod tests {
push_remote: None,
scope: None,
scope_mode: None,
repoint: None,
target_pr_repo: None,
},
);
StackState {
Expand All @@ -235,6 +239,7 @@ mod tests {
draft: None,
no_pr: None,
rerere: None,
repoint: None,
branches,
}
}
Expand Down
Loading