OXYGENHUB

bookmark set asks before losing changes; rebase names the way through a conflict 06d16dc2bc13

oxy bookmark set NAME CHANGE refuses when the bookmark would lose
changes it holds that were recorded before CHANGE, listing them, and
takes --upto (keep everything up to CHANGE) or --only, as bookmark
create does (metalgear: moving a proposal's bookmark to its resolution
dropped the Flow pipeline changes, and the push was then refused).

A rebase that would be conflicted says how to resolve it in the working
copy instead: merge the target in on the bookmark, fix the marked
files, commit.

refs #42
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 13:59 UTC
Signature
✔ signed by Andy McCall
Hash
06d16dc2bc1324b39cf4c88c3b92af72aa5630a3508b27ed0d4f61cd7fc34824
Depends on
4fe7d6045860 614541c3cebd 75167672ebcd 7b9809e67c3d 8b678b298fee c6aa5bd9d1c8 cc7a00565161 d42369abf180 ee06a6c557b7 fce44ae9ad29
Depended on by
b9bf7eec1f2d 4afab467e35f e266b7d4065c
In bookmarks
agents-in-readme 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.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.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
11 atoms, 0 tree operations, 5 files

Browse the tree at this change

modified crates/oxy-core/src/repo/history.rs +7 −1
…
1130 unchanged lines
1131 1131
                .is_some_and(|f| f.is_conflicted())
1132 1132
                || after.files.get(p).is_some_and(|f| f.is_conflicted())
1133 1133
            {
1134 -
                return fail(format!("{p} would be conflicted; resolve the target state or the conflict with #{} first", c.short(12)));
1134 +
                return fail(format!(
1135 +
                    "{p} would be conflicted, and a rebased change cannot hold a conflict. \
1136 +
                     To resolve it in the working copy, merge the target in instead: on the \
1137 +
                     bookmark holding #{}, run oxy merge with the target bookmark, fix the \
1138 +
                     marked files and oxy commit",
1139 +
                    c.short(12)
1140 +
                ));
1135 1141
            }
1136 1142
        }
1137 1143
        // Entries: the target's files, with touched paths as c leaves them.
…
68 unchanged lines
modified docs/guide.md +6 −0
…
116 unchanged lines
117 117
oxy bookmark create logo                  # the whole current state, usually what you want
118 118
```
119 119
120 +
`oxy bookmark set NAME CHANGE` asks the same when the move would lose
121 +
changes the bookmark holds that were recorded before CHANGE, and takes
122 +
`--upto` and `--only` too. After merging a proposal's target into its
123 +
bookmark to resolve conflicts, `oxy bookmark set NAME CHANGE --upto`
124 +
keeps everything the bookmark held.
125 +
120 126
`oxy bookmark switch` says when the bookmark you switch to lacks changes
121 127
that the one you left has.
122 128
…
378 unchanged lines
modified src/cli/mod.rs +16 −2
…
533 unchanged lines
534 534
    Set {
535 535
        /// The bookmark to move
536 536
        name: String,
537 -
        /// Its new heads
537 +
        /// Its new heads. A change alone may leave out changes the
538 +
        /// bookmark now holds that were recorded before it, which is
539 +
        /// refused unless --upto or --only says which is meant
538 540
        changes: Vec<String>,
541 +
        /// Everything the bookmark holds up to CHANGE..., as a Git branch
542 +
        /// moved to a commit would hold
543 +
        #[arg(long, conflicts_with = "only")]
544 +
        upto: bool,
545 +
        /// Just CHANGE... and what they depend on
546 +
        #[arg(long)]
547 +
        only: bool,
539 548
    },
540 549
    /// Remove a bookmark; its changes stay
541 550
    Delete {
…
498 unchanged lines
1040 1049
                upto,
1041 1050
                only,
1042 1051
            } => workspace::bookmark_create(&name, &changes, upto, only),
1043 -
            BookmarkCmd::Set { name, changes } => workspace::bookmark_set(&name, &changes),
1052 +
            BookmarkCmd::Set {
1053 +
                name,
1054 +
                changes,
1055 +
                upto,
1056 +
                only,
1057 +
            } => workspace::bookmark_set(&name, &changes, upto, only),
1044 1058
            BookmarkCmd::Delete { name } => workspace::bookmark_delete(&name),
1045 1059
            BookmarkCmd::List { json } => workspace::bookmark_list(json),
1046 1060
            BookmarkCmd::Switch {
…
174 unchanged lines
modified src/cli/workspace.rs +25 −2
…
1639 unchanged lines
1640 1640
    lines.join("\n")
1641 1641
}
1642 1642
1643 -
pub fn bookmark_set(name: &str, changes: &[String]) -> Result<i32> {
1643 +
pub fn bookmark_set(name: &str, changes: &[String], upto: bool, only: bool) -> Result<i32> {
1644 1644
    let repo = open()?;
1645 1645
    repo.snapshot_status("oxy bookmark set")?;
1646 -
    let heads = heads_from(&repo, changes)?;
1646 +
    let named = heads_from(&repo, changes)?;
1647 +
    // What the bookmark holds now and the new heads would leave out.
1648 +
    let current = repo
1649 +
        .bookmarks()?
1650 +
        .into_iter()
1651 +
        .find(|b| b.name == name)
1652 +
        .map(|b| b.heads)
1653 +
        .ok_or_else(|| anyhow::anyhow!("no bookmark {name}"))?;
1654 +
    let gap = repo.state_gap(&current, &named)?;
1655 +
    let heads = if upto {
1656 +
        gap.upto
1657 +
    } else if only || gap.earlier.is_empty() {
1658 +
        named
1659 +
    } else {
1660 +
        anyhow::bail!(
1661 +
            "{name} at {} would lose {} it holds now, recorded before it:\n{}\n\
1662 +
             A state is its heads and what they depend on, not everything recorded before them.\n\
1663 +
             Pass --upto to keep everything {name} holds up to {0} (as a Git branch would),\n\
1664 +
             or --only for just {0} and what it depends on.",
1665 +
            changes.join(" "),
1666 +
            plural(gap.earlier.len(), "change"),
1667 +
            listed_changes(&repo, &gap.earlier)
1668 +
        );
1669 +
    };
1647 1670
    let heads = repo.bookmark_set(name, &heads)?;
1648 1671
    println!(
1649 1672
        "{} bookmark {} {} {}",
…
199 unchanged lines
modified tests/cli_json.rs +21 −0
…
444 unchanged lines
445 445
    let out = String::from_utf8_lossy(&out);
446 446
    assert!(out.contains("only lacks 2 changes that main has"), "{out}");
447 447
    assert!(!r.join("a.txt").exists() && r.join("b.txt").exists());
448 +
449 +
    // Moving a bookmark asks the same (metalgear, oxygen#42): main at b
450 +
    // alone would lose a, which it holds and was recorded before b.
451 +
    let (code, _, err) = oxy(&r, &home, &["bookmark", "set", "main", &b]);
452 +
    assert_ne!(code, 0);
453 +
    assert!(
454 +
        err.contains("would lose 1 change") && err.contains("--upto"),
455 +
        "{err}"
456 +
    );
457 +
    assert_eq!(
458 +
        oxy(&r, &home, &["bookmark", "set", "main", &b, "--upto"]).0,
459 +
        0
460 +
    );
461 +
    let bms = json(&r, &home, &["bookmark", "list", "--json"]);
462 +
    let main = bms
463 +
        .as_array()
464 +
        .unwrap()
465 +
        .iter()
466 +
        .find(|x| x["name"] == "main")
467 +
        .unwrap();
468 +
    assert_eq!(main["heads"].as_array().unwrap().len(), 2, "a and b: {bms}");
448 469
}