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 1 month ago · 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
crates/oxy-core/src/repo/history.rs docs/guide.md src/cli/mod.rs src/cli/workspace.rs tests/cli_json.rs
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(¤t, &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
}