Skip to content

Commit 80fd8a8

Browse files
committed
feat: branch-name consistency audit — short names everywhere, like split
Audit of every command that accepts a branch name; the queue/<qname>/ prefix never has to be typed: - create: in a namespaced queue (explicit name, or branches already under queue/<name>/…) a short name creates queue/<qname>/<short>, exactly like split's segments. Names containing '/' are used as-is; plain-named queues stay plain, so no queue ever mixes conventions. - create --base, track --parent: short names resolve via a unique queue/*/<name> match (ambiguity is an error listing the candidates). - checkout <branch>: reattaches by short name within the line. - split: already compliant (the reference behavior). require_queue_name now reports whether the name was explicit, which is what decides namespacing for brand-new queues.
1 parent dbe5a78 commit 80fd8a8

3 files changed

Lines changed: 118 additions & 18 deletions

File tree

src/commands.rs

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -236,29 +236,47 @@ pub fn doctor() -> Result<()> {
236236
pub fn create(name: &str, base: Option<&str>, queue_flag: Option<&str>) -> Result<()> {
237237
git::ensure_repo()?;
238238
let trunk = meta::trunk()?;
239-
if git::branch_exists(name) {
240-
bail!("branch `{name}` already exists");
241-
}
242239
let parent = match base {
243240
Some(b) => {
244-
if !git::branch_exists(b) {
241+
let b = resolve_branch_arg(b)?;
242+
if !git::branch_exists(&b) {
245243
bail!("base branch `{b}` does not exist");
246244
}
247-
b.to_string()
245+
b
248246
}
249247
None => git::current_branch()?,
250248
};
251249
// Every queue is named: inherit when extending, otherwise ask/take one.
252-
let qname = if meta::parent(&parent).is_some() {
250+
// `namespaced` decides whether the new branch lives under queue/<name>/…:
251+
// true when the queue name is explicit or the queue already follows the
252+
// convention — so you type short names and never the prefix (as with
253+
// split), and plain-named queues stay plain.
254+
let (qname, namespaced) = if meta::parent(&parent).is_some() {
253255
let q = Queue::load()?;
254256
let line = q.line_through(&parent)?;
255257
match line_queue_name(&line) {
256-
Some(n) => n,
258+
Some(n) => {
259+
let ns = line
260+
.branches
261+
.first()
262+
.map(|b| b.starts_with("queue/"))
263+
.unwrap_or(false);
264+
(n, ns)
265+
}
257266
None => bail!("this queue has no name; run `git queue name <name>` first"),
258267
}
259268
} else {
260269
require_queue_name(queue_flag, name)?
261270
};
271+
let name = if name.contains('/') || !namespaced {
272+
name.to_string()
273+
} else {
274+
format!("queue/{qname}/{name}")
275+
};
276+
let name = name.as_str();
277+
if git::branch_exists(name) {
278+
bail!("branch `{name}` already exists");
279+
}
262280
let parent_sha = git::rev_parse(&parent)?;
263281

264282
git::create_branch(name, &parent)?;
@@ -300,10 +318,10 @@ pub fn split(delete_original: bool, queue_flag: Option<&str>) -> Result<()> {
300318
let qname = if queue.is_tracked(&branch) {
301319
match line_queue_name(&queue.line_through(&branch)?) {
302320
Some(n) => n,
303-
None => require_queue_name(queue_flag, &branch)?,
321+
None => require_queue_name(queue_flag, &branch)?.0,
304322
}
305323
} else {
306-
require_queue_name(queue_flag, &branch)?
324+
require_queue_name(queue_flag, &branch)?.0
307325
};
308326

309327
let commits = git::commits_between(&base, &branch)?;
@@ -513,7 +531,7 @@ pub fn track(
513531
bail!("working tree has uncommitted changes; commit or stash them before `track --split`");
514532
}
515533
let parent = match parent {
516-
Some(p) => p,
534+
Some(p) => resolve_branch_arg(&p)?,
517535
None => trunk.clone(),
518536
};
519537
if !git::branch_exists(&parent) {
@@ -534,7 +552,7 @@ pub fn track(
534552
let line = q.line_through(&branch)?;
535553
match line_queue_name(&line) {
536554
Some(n) => n,
537-
None => require_queue_name(queue_flag, &branch)?,
555+
None => require_queue_name(queue_flag, &branch)?.0,
538556
}
539557
};
540558
meta::set_branch_queue(&branch, &qname)?;
@@ -1358,10 +1376,10 @@ fn line_queue_name(line: &Line) -> Option<String> {
13581376
/// Ask for (or take) a queue name, mandatorily. Order: explicit flag, TTY
13591377
/// prompt, then — non-interactive with no flag — a fallback so scripts keep
13601378
/// working, announced loudly.
1361-
fn require_queue_name(flag: Option<&str>, fallback: &str) -> Result<String> {
1379+
fn require_queue_name(flag: Option<&str>, fallback: &str) -> Result<(String, bool)> {
13621380
if let Some(n) = flag {
13631381
meta::validate_queue_name(n)?;
1364-
return Ok(n.to_string());
1382+
return Ok((n.to_string(), true));
13651383
}
13661384
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
13671385
print!("Name this queue: ");
@@ -1372,13 +1390,31 @@ fn require_queue_name(flag: Option<&str>, fallback: &str) -> Result<String> {
13721390
let answer = answer.trim().to_string();
13731391
if !answer.is_empty() {
13741392
meta::validate_queue_name(&answer)?;
1375-
return Ok(answer);
1393+
return Ok((answer, true));
13761394
}
13771395
}
13781396
let fallback = fallback.replace('/', "-");
13791397
meta::validate_queue_name(&fallback)?;
13801398
eprintln!("note: queue named `{fallback}` (rename any time with `git queue name <name>`).");
1381-
Ok(fallback)
1399+
Ok((fallback, false))
1400+
}
1401+
1402+
/// Resolve a branch argument, accepting short names inside namespaced queues:
1403+
/// an exact branch name wins; otherwise a unique `queue/*/<arg>` match does.
1404+
fn resolve_branch_arg(arg: &str) -> Result<String> {
1405+
if git::branch_exists(arg) {
1406+
return Ok(arg.to_string());
1407+
}
1408+
let suffix = format!("/{arg}");
1409+
let matches: Vec<String> = meta::tracked_branches()
1410+
.into_iter()
1411+
.filter(|b| b.ends_with(&suffix))
1412+
.collect();
1413+
match matches.as_slice() {
1414+
[one] => Ok(one.clone()),
1415+
[] => Ok(arg.to_string()), // let the caller produce its natural error
1416+
many => bail!("`{arg}` is ambiguous: {}", many.join(", ")),
1417+
}
13821418
}
13831419

13841420
/// The outcome of reconciling one line's PRs.
@@ -1954,8 +1990,16 @@ pub fn checkout(arg: &str) -> Result<()> {
19541990
};
19551991
let top = line.branches.last().unwrap().clone();
19561992

1957-
// Checking out a branch of the line reattaches and ends the session.
1958-
if line.branches.iter().any(|b| b == arg) || arg == line.base {
1993+
// Checking out a branch of the line reattaches and ends the session
1994+
// (short names resolve inside namespaced queues).
1995+
let reattach = line
1996+
.branches
1997+
.iter()
1998+
.find(|b| *b == arg || b.ends_with(&format!("/{arg}")))
1999+
.cloned()
2000+
.or_else(|| (arg == line.base).then(|| line.base.clone()));
2001+
if let Some(target) = reattach {
2002+
let arg = target.as_str();
19592003
if !git::tracked_clean() {
19602004
bail!("stage or tracked files have changes; commit or stash them first");
19612005
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ struct Cli {
3434
enum Command {
3535
/// Create a new branch queued after the current one.
3636
#[command(
37-
long_about = "Creates `<name>` at the current branch's tip (or at `--base <branch>`'s tip) and records it as the next branch of the queue. Extending a queue inherits its name; starting a new one asks for a name (or takes `--queue`). The front PR of a queue targets its base branchwhich is how queues can be built on release or bugfix branches, not just trunk. Make commits, then `git queue submit` opens the numbered PRs."
37+
long_about = "Creates the next branch of the queue at the current branch's tip (or at `--base <branch>`'s tip). In a namespaced queue — one with an explicit name, or whose branches already live under queue/<name>/… — you give the short name and the branch is created as queue/<name>/<short>, exactly like split's segments; names containing `/` are used as-is, and plain-named queues stay plain. Extending a queue inherits its name; starting a new one asks (or takes `--queue`). The front PR targets the base branch, which is how queues can be built on release branches. Branch arguments elsewhere (`--base`, `track --parent`, `checkout`) accept the same short names."
3838
)]
3939
Create {
4040
/// Name of the new branch.

tests/integration.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,3 +1670,59 @@ fn setup_yes_installs_hooks_and_gate_and_undo_reverses() {
16701670
.unwrap();
16711671
assert!(!gate.status.success(), "gate should be unset");
16721672
}
1673+
1674+
#[test]
1675+
fn create_namespaces_branches_and_short_names_resolve() {
1676+
let tmp = new_repo();
1677+
let dir = tmp.path();
1678+
// Explicit queue name => namespaced branches from short names.
1679+
queue(dir)
1680+
.args(["create", "fix-a", "--queue", "pay"])
1681+
.assert()
1682+
.success();
1683+
assert_eq!(
1684+
git_out(dir, &["rev-parse", "--abbrev-ref", "HEAD"]),
1685+
"queue/pay/fix-a"
1686+
);
1687+
commit(dir, "a.txt");
1688+
// Extending inherits the namespace, still from a short name.
1689+
queue(dir).args(["create", "fix-b"]).assert().success();
1690+
assert_eq!(
1691+
git_out(dir, &["rev-parse", "--abbrev-ref", "HEAD"]),
1692+
"queue/pay/fix-b"
1693+
);
1694+
assert_eq!(
1695+
git_out(dir, &["config", "branch.queue/pay/fix-b.queueParent"]),
1696+
"queue/pay/fix-a"
1697+
);
1698+
commit(dir, "b.txt");
1699+
1700+
// checkout reattaches by short name.
1701+
let c = sha(dir, "queue/pay/fix-a");
1702+
queue(dir).args(["checkout", &c]).assert().success();
1703+
queue(dir).args(["checkout", "fix-b"]).assert().success();
1704+
assert_eq!(
1705+
git_out(dir, &["rev-parse", "--abbrev-ref", "HEAD"]),
1706+
"queue/pay/fix-b"
1707+
);
1708+
1709+
// track --parent resolves a short name too.
1710+
git(dir, &["checkout", "-q", "-b", "hotfix", "queue/pay/fix-b"]);
1711+
commit(dir, "h.txt");
1712+
queue(dir)
1713+
.args(["track", "--parent", "fix-b", "--no-stamp-ids"])
1714+
.assert()
1715+
.success();
1716+
assert_eq!(
1717+
git_out(dir, &["config", "branch.hotfix.queueParent"]),
1718+
"queue/pay/fix-b"
1719+
);
1720+
1721+
// Plain-named queues stay plain (fallback naming, no prefix).
1722+
git(dir, &["checkout", "-q", "main"]);
1723+
queue(dir).args(["create", "plain"]).assert().success();
1724+
assert_eq!(
1725+
git_out(dir, &["rev-parse", "--abbrev-ref", "HEAD"]),
1726+
"plain"
1727+
);
1728+
}

0 commit comments

Comments
 (0)