bookmark set: fast-forward onto work built on the newest head 4afab467e35f
Moving a bookmark onto a change built on its newest head was refused when the bookmark also held changes the new one does not depend on, such as early independent changes every bookmark carries as heads (feedback #37). When the new heads hold the bookmark's newest head and add to it, those are kept, as --upto does, and the command says so. A head the bookmark already holds is still refused without --upto or --only. closes #78
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-06 17:32 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
4afab467e35f35023c84f531f79824cb10e31c242baa279de8b934177f8c84f1- Depends on
- 06d16dc2bc13 4fe7d6045860 c6aa5bd9d1c8 ee06a6c557b7 fbda43e7d8ed fce44ae9ad29
- Depended on by
- nothing yet
- 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
- 7 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified docs/guide.md +1 −1
| … | 94 unchanged lines |
||
| 95 | 95 | oxy merge feature # main now holds both; working copy follows if clean |
|
| 96 | 96 | oxy bookmark list # ● current, heads, ↑n/↓n against origin |
|
| 97 | 97 | oxy push -b feature # publish a bookmark other than the current one |
|
| 98 | - | oxy bookmark set feature 658990aa # move it; oxy bookmark delete feature to drop it |
|
| 98 | + | oxy bookmark set feature 658990aa # move it (onto work built on it: kept heads stay); delete drops it |
|
| 99 | 99 | ``` |
|
| 100 | 100 | ||
| 101 | 101 | `oxy checkout feature` materialises a bookmark without switching to it, and |
|
| … | 405 unchanged lines |
modified src/cli/workspace.rs +32 −0
| … | 1651 unchanged lines |
||
| 1652 | 1652 | .map(|b| b.heads) |
|
| 1653 | 1653 | .ok_or_else(|| anyhow::anyhow!("no bookmark {name}"))?; |
|
| 1654 | 1654 | let gap = repo.state_gap(¤t, &named)?; |
|
| 1655 | + | // A fast-forward: the new heads hold the bookmark's newest head, so |
|
| 1656 | + | // what they leave out is older work nothing since has built on, such |
|
| 1657 | + | // as an early change no later one depends on (oxygen#78). Keep it, as |
|
| 1658 | + | // --upto does, and say so. |
|
| 1659 | + | let newest = current |
|
| 1660 | + | .iter() |
|
| 1661 | + | .filter_map(|h| repo.store().get_change_meta(h).ok().map(|m| (m.hlc, *h))) |
|
| 1662 | + | .max() |
|
| 1663 | + | .map(|(_, h)| h); |
|
| 1664 | + | // Ahead, too: a head the bookmark holds already (it would only drop |
|
| 1665 | + | // the others) is not a move forward. |
|
| 1666 | + | let ahead = repo |
|
| 1667 | + | .load_state_history(¤t) |
|
| 1668 | + | .is_ok_and(|st| named.iter().any(|h| !st.contains(h))); |
|
| 1669 | + | let forward = !only |
|
| 1670 | + | && ahead |
|
| 1671 | + | && !gap.earlier.is_empty() |
|
| 1672 | + | && newest.is_some_and(|n| { |
|
| 1673 | + | repo.load_state_history(&named) |
|
| 1674 | + | .is_ok_and(|st| st.contains(&n)) |
|
| 1675 | + | }); |
|
| 1655 | 1676 | let heads = if upto { |
|
| 1677 | + | gap.upto |
|
| 1678 | + | } else if forward { |
|
| 1679 | + | println!( |
|
| 1680 | + | " {} {}", |
|
| 1681 | + | ui::note(), |
|
| 1682 | + | ui::dim(format!( |
|
| 1683 | + | "{} builds on {name}'s newest head; keeping {} it holds that {0} does not depend on", |
|
| 1684 | + | changes.join(" "), |
|
| 1685 | + | plural(gap.earlier.len(), "change") |
|
| 1686 | + | )) |
|
| 1687 | + | ); |
|
| 1656 | 1688 | gap.upto |
|
| 1657 | 1689 | } else if only || gap.earlier.is_empty() { |
|
| 1658 | 1690 | named |
|
| … | 213 unchanged lines |
modified tests/cli_json.rs +35 −1
| … | 448 unchanged lines |
||
| 449 | 449 | // Moving a bookmark asks the same (metalgear, oxygen#42): main at b |
|
| 450 | 450 | // alone would lose a, which it holds and was recorded before b. |
|
| 451 | 451 | let (code, _, err) = oxy(&r, &home, &["bookmark", "set", "main", &b]); |
|
| 452 | - | assert_ne!(code, 0); |
|
| 452 | + | assert_ne!(code, 0, "b is a head main holds, not a move forward"); |
|
| 453 | 453 | assert!( |
|
| 454 | 454 | err.contains("would lose 1 change") && err.contains("--upto"), |
|
| 455 | 455 | "{err}" |
|
| … | 10 unchanged lines |
||
| 466 | 466 | .find(|x| x["name"] == "main") |
|
| 467 | 467 | .unwrap(); |
|
| 468 | 468 | assert_eq!(main["heads"].as_array().unwrap().len(), 2, "a and b: {bms}"); |
|
| 469 | + | ||
| 470 | + | // A fast-forward (oxygen#78): y holds a and b; b2 is built on b, its |
|
| 471 | + | // newest head. Moving y to b2 keeps a, which b2 does not depend on. |
|
| 472 | + | assert_eq!( |
|
| 473 | + | oxy(&r, &home, &["bookmark", "switch", "main", "--discard"]).0, |
|
| 474 | + | 0 |
|
| 475 | + | ); |
|
| 476 | + | assert_eq!(oxy(&r, &home, &["bookmark", "create", "y"]).0, 0); |
|
| 477 | + | write(&r, "b.txt", "b2\n"); |
|
| 478 | + | assert_eq!(oxy(&r, &home, &["commit", "-m", "b2"]).0, 0); |
|
| 479 | + | let log = json(&r, &home, &["log", "--json"]); |
|
| 480 | + | let b2 = log["changes"] |
|
| 481 | + | .as_array() |
|
| 482 | + | .unwrap() |
|
| 483 | + | .iter() |
|
| 484 | + | .find(|c| c["message"] == "b2") |
|
| 485 | + | .unwrap()["hash"] |
|
| 486 | + | .as_str() |
|
| 487 | + | .unwrap()[..12] |
|
| 488 | + | .to_string(); |
|
| 489 | + | let (code, out, err) = oxy(&r, &home, &["bookmark", "set", "y", &format!("#{b2}")]); |
|
| 490 | + | assert_eq!(code, 0, "{err}"); |
|
| 491 | + | assert!( |
|
| 492 | + | String::from_utf8_lossy(&out).contains("builds on y's newest head"), |
|
| 493 | + | "says why" |
|
| 494 | + | ); |
|
| 495 | + | let bms = json(&r, &home, &["bookmark", "list", "--json"]); |
|
| 496 | + | let y = bms |
|
| 497 | + | .as_array() |
|
| 498 | + | .unwrap() |
|
| 499 | + | .iter() |
|
| 500 | + | .find(|x| x["name"] == "y") |
|
| 501 | + | .unwrap(); |
|
| 502 | + | assert_eq!(y["heads"].as_array().unwrap().len(), 2, "a and b2: {bms}"); |
|
| 469 | 503 | } |