bookmark: show a bookmark's heads; bookmark() in revsets bf3a67819b59
oxy bookmark show [NAME] lists each head, newest first, with its id, hash, author, time and subject (--json too); bookmark list -v does it for every bookmark, and bookmark list now names the newest head's subject rather than the first. bookmark(NAME) in a revset is a local or remote bookmark's heads and nothing else. log -r's help and the guide say a bookmark name is a revset for its heads. closes #73
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-05 18:32 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
bf3a67819b59931bd91f38620a3f3fc26febe7af53c32ec2b1dfb03d19e67984- Depends on
- 14edbaaf5291 2c9ddac3c387 47ce8b8e2d4b 61e3bce7ea5b 75167672ebcd 7993d465675b 7b9809e67c3d 8ad9dfe16303 94084087a876 982864068a59 c6aa5bd9d1c8 d42369abf180 ee06a6c557b7 fbda43e7d8ed fce44ae9ad29
- Depended on by
- 42ccf504aa50 72484195899a
- In bookmarks
- bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.10.0 v0.11.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 16 atoms, 0 tree operations, 6 files
Browse the tree at this change
modified crates/oxy-core/src/repo/revset.rs +17 −0
| … | 462 unchanged lines |
||
| 463 | 463 | let kind = self.text_arg(name, args, 0)?.to_string(); |
|
| 464 | 464 | self.attested(within, &kind) |
|
| 465 | 465 | } |
|
| 466 | + | // A bookmark's heads by name: what a bare bookmark symbol means, |
|
| 467 | + | // without falling through to tags and change prefixes. |
|
| 468 | + | "bookmark" => { |
|
| 469 | + | self.arity(name, args, 1)?; |
|
| 470 | + | let b = self.text_arg(name, args, 0)?; |
|
| 471 | + | let view = self.repo.view()?; |
|
| 472 | + | let st = match view.bookmarks.get(b) { |
|
| 473 | + | Some(st) => *st, |
|
| 474 | + | None => match remote_bookmark(&view, b) { |
|
| 475 | + | Some((_, _, st)) => st, |
|
| 476 | + | None => return Err(err(format!("bookmark({b}): no such bookmark"))), |
|
| 477 | + | }, |
|
| 478 | + | }; |
|
| 479 | + | let heads = self.repo.store().get_state(&st)?.heads; |
|
| 480 | + | self.ensure(&heads)?; |
|
| 481 | + | Ok(heads.into_iter().collect()) |
|
| 482 | + | } |
|
| 466 | 483 | "built" => { |
|
| 467 | 484 | self.arity(name, args, 1)?; |
|
| 468 | 485 | let job = self.text_arg(name, args, 0)?.to_string(); |
|
| … | 277 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +27 −0
| … | 7736 unchanged lines |
||
| 7737 | 7737 | app.submodule_add(&lib_url, "libs/lib", None).unwrap(); |
|
| 7738 | 7738 | assert!(app.unpinned_modules().unwrap().is_empty(), "added now"); |
|
| 7739 | 7739 | } |
|
| 7740 | + | ||
| 7741 | + | #[test] |
|
| 7742 | + | fn the_bookmark_revset_function_names_only_bookmarks() { |
|
| 7743 | + | // oxygen#73: bookmark(NAME) is a bookmark's heads, never a tag or prefix. |
|
| 7744 | + | hermetic(); |
|
| 7745 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 7746 | + | let repo = Repo::init(dir.path()).unwrap(); |
|
| 7747 | + | write(dir.path(), "a.txt", b"a\n"); |
|
| 7748 | + | let a = repo.commit("A", &[]).unwrap(); |
|
| 7749 | + | write(dir.path(), "b.txt", b"b\n"); |
|
| 7750 | + | let b = repo.commit("B", &[]).unwrap(); |
|
| 7751 | + | let mut heads = repo.revset("bookmark(main)").unwrap().hashes; |
|
| 7752 | + | heads.sort(); |
|
| 7753 | + | let mut want = repo.bookmark_heads().unwrap(); |
|
| 7754 | + | want.sort(); |
|
| 7755 | + | assert_eq!(heads, want); |
|
| 7756 | + | assert!(heads.contains(&b)); |
|
| 7757 | + | assert_eq!(repo.revset("main").unwrap().hashes.len(), heads.len()); |
|
| 7758 | + | // A change prefix is a symbol, not a bookmark. |
|
| 7759 | + | let prefix = repo.store().get_change_meta(&a).unwrap().change_id.short(8); |
|
| 7760 | + | assert!(repo.revset(&prefix).is_ok()); |
|
| 7761 | + | let e = repo |
|
| 7762 | + | .revset(&format!("bookmark({prefix})")) |
|
| 7763 | + | .unwrap_err() |
|
| 7764 | + | .to_string(); |
|
| 7765 | + | assert!(e.contains("no such bookmark"), "{e}"); |
|
| 7766 | + | } |
modified docs/guide.md +3 −0
| … | 117 unchanged lines |
||
| 118 | 118 | oxy bookmark switch main |
|
| 119 | 119 | oxy merge feature # main now holds both; working copy follows if clean |
|
| 120 | 120 | oxy bookmark list # ● current, heads, ↑n/↓n against origin |
|
| 121 | + | oxy bookmark show feature # each head, newest first (list -v: every bookmark's) |
|
| 121 | 122 | oxy push -b feature # publish a bookmark other than the current one |
|
| 122 | 123 | oxy bookmark set feature 658990aa # move it; oxy bookmark delete feature to drop it |
|
| 123 | 124 | ``` |
|
| 124 | 125 | ||
| 125 | 126 | `oxy checkout feature` materialises a bookmark without switching to it, and |
|
| 126 | 127 | a bookmark name works wherever a change is expected when it has one head. |
|
| 128 | + | In a revset it means the bookmark's heads (`oxy log -r feature`); |
|
| 129 | + | `bookmark(feature)` means only the bookmark, never a tag or change prefix. |
|
| 127 | 130 | Each workspace remembers its own bookmark, so two directories can sit on |
|
| 128 | 131 | two bookmarks of one repository. |
|
| 129 | 132 | ||
| … | 520 unchanged lines |
modified docs/revset_query_language.md +1 −0
| … | 48 unchanged lines |
||
| 49 | 49 | | Symbol | Means | |
|
| 50 | 50 | |---|---| |
|
| 51 | 51 | | `main`, `v0.3.1` | the heads of that bookmark or tag | |
|
| 52 | + | | `bookmark(main)`, `bookmark(origin/main)` | the heads of that bookmark only; an error if there is none (a bare name falls through to tags and change prefixes) | |
|
| 52 | 53 | | `origin/main` | the heads of a remote-tracking bookmark | |
|
| 53 | 54 | | `8c5ea12a` | the change with that ChangeId prefix | |
|
| 54 | 55 | | `#fc68459c` | the change with that hash prefix | |
|
| … | 108 unchanged lines |
modified src/cli/mod.rs +21 −3
| … | 156 unchanged lines |
||
| 157 | 157 | }, |
|
| 158 | 158 | /// List changes reachable from the bookmark and the working copy |
|
| 159 | 159 | Log { |
|
| 160 | - | /// Show the changes a revset selects: `main..feature & author(andy)`, |
|
| 161 | - | /// `touches("src/**") & date(-7d..)` (see docs/revset_query_language.md) |
|
| 160 | + | /// Show the changes a revset selects: `main` (a bookmark name means its |
|
| 161 | + | /// heads; `bookmark(main)` only ever the bookmark), `main..feature & |
|
| 162 | + | /// author(andy)`, `touches("src/**") & date(-7d..)` (see |
|
| 163 | + | /// docs/revset_query_language.md) |
|
| 162 | 164 | #[arg(short = 'r', long, value_name = "REVSET", conflicts_with_all = ["path", "lines"])] |
|
| 163 | 165 | revset: Option<String>, |
|
| 164 | 166 | /// Show at most this many changes |
|
| … | 454 unchanged lines |
||
| 619 | 621 | /// Also list remotes' bookmarks that have no local bookmark |
|
| 620 | 622 | #[arg(long, short)] |
|
| 621 | 623 | all: bool, |
|
| 624 | + | /// Each head of every bookmark, as `bookmark show` prints them |
|
| 625 | + | #[arg(long, short)] |
|
| 626 | + | verbose: bool, |
|
| 622 | 627 | /// Machine-readable output: one JSON document on stdout |
|
| 623 | 628 | #[arg(long)] |
|
| 624 | 629 | json: bool, |
|
| 625 | 630 | }, |
|
| 631 | + | /// A bookmark's heads: each change nothing else on it depends on, |
|
| 632 | + | /// newest first. A bookmark name is also a revset for its heads |
|
| 633 | + | /// (`oxy log -r NAME`, or `bookmark(NAME)` to mean only the bookmark) |
|
| 634 | + | Show { |
|
| 635 | + | /// The bookmark, or REMOTE/NAME (default: the current one) |
|
| 636 | + | name: Option<String>, |
|
| 637 | + | /// Machine-readable output: one JSON document on stdout |
|
| 638 | + | #[arg(long)] |
|
| 639 | + | json: bool, |
|
| 640 | + | }, |
|
| 626 | 641 | /// Make NAME this workspace's bookmark and check it out. Uncommitted work stops it; --carry takes the work along, --discard drops it |
|
| 627 | 642 | Switch { |
|
| 628 | 643 | /// The bookmark to work on; one only a remote has (NAME or REMOTE/NAME) is created from it |
|
| … | 554 unchanged lines |
||
| 1183 | 1198 | only, |
|
| 1184 | 1199 | } => workspace::bookmark_set(&name, &changes, upto, only), |
|
| 1185 | 1200 | BookmarkCmd::Delete { name } => workspace::bookmark_delete(&name), |
|
| 1186 | - | BookmarkCmd::List { all, json } => workspace::bookmark_list(all, json), |
|
| 1201 | + | BookmarkCmd::List { all, verbose, json } => { |
|
| 1202 | + | workspace::bookmark_list(all, verbose, json) |
|
| 1203 | + | } |
|
| 1204 | + | BookmarkCmd::Show { name, json } => workspace::bookmark_show(name.as_deref(), json), |
|
| 1187 | 1205 | BookmarkCmd::Switch { |
|
| 1188 | 1206 | name, |
|
| 1189 | 1207 | discard, |
|
| … | 194 unchanged lines |
modified src/cli/workspace.rs +110 −12
| … | 2311 unchanged lines |
||
| 2312 | 2312 | Ok(0) |
|
| 2313 | 2313 | } |
|
| 2314 | 2314 | ||
| 2315 | - | pub fn bookmark_list(all: bool, as_json: bool) -> Result<i32> { |
|
| 2315 | + | /// Heads with their metadata, newest first. |
|
| 2316 | + | fn heads_newest_first( |
|
| 2317 | + | repo: &Repo, |
|
| 2318 | + | heads: &[oxy_core::hash::ChangeHash], |
|
| 2319 | + | ) -> Vec<(oxy_core::hash::ChangeHash, oxy_core::state::ChangeMeta)> { |
|
| 2320 | + | let mut out: Vec<_> = heads |
|
| 2321 | + | .iter() |
|
| 2322 | + | .filter_map(|h| repo.store().get_change_meta(h).ok().map(|m| (*h, m))) |
|
| 2323 | + | .collect(); |
|
| 2324 | + | out.sort_by_key(|(_, m)| std::cmp::Reverse(m.hlc)); |
|
| 2325 | + | out |
|
| 2326 | + | } |
|
| 2327 | + | ||
| 2328 | + | /// One head as a line: id, hash, author, when, subject. |
|
| 2329 | + | fn print_head( |
|
| 2330 | + | repo: &Repo, |
|
| 2331 | + | indent: &str, |
|
| 2332 | + | h: &oxy_core::hash::ChangeHash, |
|
| 2333 | + | m: &oxy_core::state::ChangeMeta, |
|
| 2334 | + | ) { |
|
| 2335 | + | println!( |
|
| 2336 | + | "{indent}{} {} {}{}{}{}{}", |
|
| 2337 | + | ui::id(m.change_id.short(8)), |
|
| 2338 | + | ui::dim(format!("#{}", &h.to_hex()[..8])), |
|
| 2339 | + | author_name(repo, m), |
|
| 2340 | + | ui::sep(), |
|
| 2341 | + | ui::dim(ui::ago(m.hlc.wall_ms)), |
|
| 2342 | + | ui::sep(), |
|
| 2343 | + | ui::subject(subject_of(&m.message)) |
|
| 2344 | + | ); |
|
| 2345 | + | } |
|
| 2346 | + | ||
| 2347 | + | pub fn bookmark_show(name: Option<&str>, as_json: bool) -> Result<i32> { |
|
| 2316 | 2348 | let repo = open()?; |
|
| 2349 | + | let name = name.unwrap_or(&repo.config.bookmark).to_string(); |
|
| 2350 | + | let view = repo.view()?; |
|
| 2351 | + | let st = match view.bookmarks.get(&name) { |
|
| 2352 | + | Some(st) => *st, |
|
| 2353 | + | None => match oxy_core::repo::remote_bookmark(&view, &name) { |
|
| 2354 | + | Some((_, _, st)) => st, |
|
| 2355 | + | None => anyhow::bail!("no bookmark {name}: oxy bookmark list --all lists them"), |
|
| 2356 | + | }, |
|
| 2357 | + | }; |
|
| 2358 | + | let heads = repo.store().get_state(&st)?.heads; |
|
| 2359 | + | let shown = heads_newest_first(&repo, &heads); |
|
| 2360 | + | if as_json { |
|
| 2361 | + | println!( |
|
| 2362 | + | "{}", |
|
| 2363 | + | json::obj([ |
|
| 2364 | + | ("name", json::s(&name)), |
|
| 2365 | + | ("current", json::b(name == repo.config.bookmark)), |
|
| 2366 | + | ( |
|
| 2367 | + | "heads", |
|
| 2368 | + | json::arr(shown.iter().map(|(h, m)| { |
|
| 2369 | + | json::obj([ |
|
| 2370 | + | ("hash", json::s(&h.to_hex())), |
|
| 2371 | + | ("id", json::s(&m.change_id.to_string())), |
|
| 2372 | + | ("author", json::s(&author_name(&repo, m))), |
|
| 2373 | + | ("time_ms", json::n(m.hlc.wall_ms)), |
|
| 2374 | + | ("subject", json::s(subject_of(&m.message))), |
|
| 2375 | + | ]) |
|
| 2376 | + | })) |
|
| 2377 | + | ), |
|
| 2378 | + | ]) |
|
| 2379 | + | ); |
|
| 2380 | + | return Ok(0); |
|
| 2381 | + | } |
|
| 2382 | + | println!( |
|
| 2383 | + | "{} {}{}{}", |
|
| 2384 | + | ui::ok(ui::glyph("●", "*")), |
|
| 2385 | + | ui::bookmark(&name), |
|
| 2386 | + | ui::sep(), |
|
| 2387 | + | ui::dim(plural(heads.len(), "head")) |
|
| 2388 | + | ); |
|
| 2389 | + | for (h, m) in &shown { |
|
| 2390 | + | print_head(&repo, " ", h, m); |
|
| 2391 | + | } |
|
| 2392 | + | if heads.len() > 1 { |
|
| 2393 | + | println!( |
|
| 2394 | + | " {}", |
|
| 2395 | + | ui::dim("None depends on another: the bookmark is all of them and their dependencies.") |
|
| 2396 | + | ); |
|
| 2397 | + | } |
|
| 2398 | + | Ok(0) |
|
| 2399 | + | } |
|
| 2400 | + | ||
| 2401 | + | pub fn bookmark_list(all: bool, verbose: bool, as_json: bool) -> Result<i32> { |
|
| 2402 | + | let repo = open()?; |
|
| 2317 | 2403 | let remote_only = repo.remote_only_bookmarks()?; |
|
| 2318 | 2404 | if as_json { |
|
| 2319 | 2405 | let mut items: Vec<String> = repo |
|
| … | 37 unchanged lines |
||
| 2357 | 2443 | if let Some(n) = b.behind.filter(|n| *n > 0) { |
|
| 2358 | 2444 | tail += &ui::warn(format!(" {}{n}", ui::glyph("↓", "-"))); |
|
| 2359 | 2445 | } |
|
| 2360 | - | let head = b |
|
| 2361 | - | .heads |
|
| 2362 | - | .first() |
|
| 2363 | - | .map(|h| { |
|
| 2364 | - | repo.store() |
|
| 2365 | - | .get_change(h) |
|
| 2366 | - | .map(|c| ui::subject(subject_of(&c.message))) |
|
| 2367 | - | .unwrap_or_default() |
|
| 2368 | - | }) |
|
| 2369 | - | .unwrap_or_else(|| ui::dim("(empty)")); |
|
| 2446 | + | let shown = heads_newest_first(&repo, &b.heads); |
|
| 2447 | + | // The newest head's subject, so a busy bookmark shows its latest work. |
|
| 2448 | + | let head = match shown.first() { |
|
| 2449 | + | Some(_) if verbose => String::new(), |
|
| 2450 | + | Some((_, m)) if b.heads.len() > 1 => format!( |
|
| 2451 | + | "{} {}", |
|
| 2452 | + | ui::dim("newest"), |
|
| 2453 | + | ui::subject(subject_of(&m.message)) |
|
| 2454 | + | ), |
|
| 2455 | + | Some((_, m)) => ui::subject(subject_of(&m.message)), |
|
| 2456 | + | None => ui::dim("(empty)"), |
|
| 2457 | + | }; |
|
| 2458 | + | let head = if head.is_empty() { |
|
| 2459 | + | head |
|
| 2460 | + | } else { |
|
| 2461 | + | format!(" {head}") |
|
| 2462 | + | }; |
|
| 2370 | 2463 | println!( |
|
| 2371 | - | "{mark} {}{}{}{tail} {head}", |
|
| 2464 | + | "{mark} {}{}{}{tail}{head}", |
|
| 2372 | 2465 | ui::bookmark(&b.name), |
|
| 2373 | 2466 | ui::sep(), |
|
| 2374 | 2467 | ui::dim(plural(b.heads.len(), "head")) |
|
| 2375 | 2468 | ); |
|
| 2469 | + | if verbose { |
|
| 2470 | + | for (h, m) in &shown { |
|
| 2471 | + | print_head(&repo, " ", h, m); |
|
| 2472 | + | } |
|
| 2473 | + | } |
|
| 2376 | 2474 | } |
|
| 2377 | 2475 | if remote_only.is_empty() { |
|
| 2378 | 2476 | return Ok(0); |
|
| … | 190 unchanged lines |