feat: refresh diffs when selected jj bookmarks move
This commit is contained in:
+231
-22
@@ -1056,6 +1056,9 @@ type Snapshot = BTreeMap<PathBuf, FileData>;
|
||||
|
||||
struct PreparedComparison {
|
||||
id: u64,
|
||||
selected: [Revision; 2],
|
||||
selected_ids: [Option<String>; 2],
|
||||
mode: ComparisonMode,
|
||||
left: Snapshot,
|
||||
right: Snapshot,
|
||||
files: Vec<(PathBuf, Option<PathBuf>, FileStatus)>,
|
||||
@@ -1068,6 +1071,12 @@ struct PreparedComparison {
|
||||
editable_after: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CheckoutRefresh {
|
||||
Checkout(Comparison),
|
||||
Bookmark(Comparison),
|
||||
}
|
||||
|
||||
pub struct Repository {
|
||||
root: PathBuf,
|
||||
kind: RepoKind,
|
||||
@@ -1145,7 +1154,7 @@ impl Repository {
|
||||
notices: &mut Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
if self.kind == RepoKind::Jujutsu {
|
||||
let template = r#""[" ++ json(commit_id) ++ "," ++ json(change_id) ++ "," ++ json(description) ++ "," ++ json(local_bookmarks.map(|b| b.name())) ++ "," ++ json(remote_bookmarks.map(|b| stringify(b.name() ++ "@" ++ b.remote()))) ++ "]\n""#;
|
||||
let template = r#""[" ++ json(commit_id) ++ "," ++ json(change_id) ++ "," ++ json(description) ++ "," ++ json(local_bookmarks.map(|b| b.name())) ++ "," ++ json(remote_bookmarks.map(|b| b.name())) ++ "," ++ json(remote_bookmarks.map(|b| b.remote())) ++ "]\n""#;
|
||||
let revset = if root == self.root {
|
||||
"all()"
|
||||
} else {
|
||||
@@ -1154,15 +1163,15 @@ impl Repository {
|
||||
let output =
|
||||
text(self.jj(root, &["log", "--no-graph", "-r", revset, "-T", template])?)?;
|
||||
for line in output.lines() {
|
||||
let (id, change, description, mut bookmarks, remote_bookmarks): (
|
||||
let (id, change, description, bookmarks, remote_names, remotes): (
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
) = serde_json::from_str(line)
|
||||
.map_err(|e| format!("Cannot decode Jujutsu revision: {e}"))?;
|
||||
bookmarks.extend(remote_bookmarks);
|
||||
let target = RevisionTarget::Commit(id.clone());
|
||||
entries.push(Revision {
|
||||
target: target.clone(),
|
||||
@@ -1170,10 +1179,22 @@ impl Repository {
|
||||
description: description.trim_end_matches('\n').into(),
|
||||
category: Category::Revisions,
|
||||
});
|
||||
for name in bookmarks {
|
||||
for (name, remote) in bookmarks.into_iter().map(|name| (name, None)).chain(
|
||||
remote_names
|
||||
.into_iter()
|
||||
.zip(remotes)
|
||||
.map(|(name, remote)| (name, Some(remote))),
|
||||
) {
|
||||
let label = remote
|
||||
.as_ref()
|
||||
.map_or_else(|| name.clone(), |remote| format!("{name}@{remote}"));
|
||||
entries.push(Revision {
|
||||
target: target.clone(),
|
||||
label: name,
|
||||
target: if root == self.root {
|
||||
RevisionTarget::Bookmark { name, remote }
|
||||
} else {
|
||||
target.clone()
|
||||
},
|
||||
label,
|
||||
description: format!("{change} {id} {}", description.trim_end()),
|
||||
category: Category::Bookmarks,
|
||||
});
|
||||
@@ -1651,8 +1672,63 @@ impl Repository {
|
||||
};
|
||||
Ok((before, after))
|
||||
}
|
||||
fn bookmark_ids(&self, targets: [&RevisionTarget; 2]) -> Result<[Option<String>; 2], String> {
|
||||
let mut ids = [None, None];
|
||||
if !targets
|
||||
.iter()
|
||||
.any(|target| matches!(target, RevisionTarget::Bookmark { .. }))
|
||||
{
|
||||
return Ok(ids);
|
||||
}
|
||||
if self.kind != RepoKind::Jujutsu {
|
||||
return Err("Bookmarks require a Jujutsu repository".into());
|
||||
}
|
||||
// Resolve both endpoints from one operation, comparing raw names rather
|
||||
// than interpreting bookmark names as revsets or splitting on '@'.
|
||||
let template = r#""[" ++ json(name) ++ "," ++ json(remote) ++ "," ++ json(conflict) ++ "," ++ if(conflict, "null", json(normal_target.commit_id())) ++ "]\n""#;
|
||||
let output = text(self.jj(
|
||||
&self.root,
|
||||
&["bookmark", "list", "--all-remotes", "-T", template],
|
||||
)?)?;
|
||||
for line in output.lines() {
|
||||
let (name, remote, conflict, id): (String, Option<String>, bool, Option<String>) =
|
||||
serde_json::from_str(line)
|
||||
.map_err(|e| format!("Cannot decode Jujutsu bookmark: {e}"))?;
|
||||
for (index, target) in targets.iter().enumerate() {
|
||||
if let RevisionTarget::Bookmark {
|
||||
name: selected_name,
|
||||
remote: selected_remote,
|
||||
} = target
|
||||
&& selected_name == &name
|
||||
&& selected_remote == &remote
|
||||
{
|
||||
if conflict {
|
||||
return Err(format!("Bookmark {name} is conflicted"));
|
||||
}
|
||||
ids[index] = id.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (index, target) in targets.iter().enumerate() {
|
||||
if let RevisionTarget::Bookmark { name, remote } = target
|
||||
&& ids[index].is_none()
|
||||
{
|
||||
let label = remote
|
||||
.as_ref()
|
||||
.map_or_else(|| name.clone(), |remote| format!("{name}@{remote}"));
|
||||
return Err(format!("Bookmark {label} is missing or deleted"));
|
||||
}
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
fn resolve(&mut self, target: &RevisionTarget) -> Result<(PathBuf, String), String> {
|
||||
let id = match target {
|
||||
RevisionTarget::Bookmark { .. } => {
|
||||
return Ok((
|
||||
self.store.clone(),
|
||||
self.bookmark_ids([target, target])?[0].take().unwrap(),
|
||||
));
|
||||
}
|
||||
RevisionTarget::Commit(id) | RevisionTarget::RemoteCommit { id, .. } => {
|
||||
if !matches!(id.len(), 40 | 64)
|
||||
|| (self.kind == RepoKind::Jujutsu && id.len() != 40)
|
||||
@@ -2321,27 +2397,34 @@ impl Repository {
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Comparison, String> {
|
||||
self.check_cancelled()?;
|
||||
let bookmarks = self.bookmark_ids([&before.target, &after.target])?;
|
||||
let mut selected_ids = bookmarks.clone();
|
||||
for (index, revision) in [before, after].iter().enumerate() {
|
||||
if revision.target != RevisionTarget::Working && selected_ids[index].is_none() {
|
||||
selected_ids[index] = Some(self.resolve(&revision.target)?.1);
|
||||
}
|
||||
}
|
||||
let targets = selected_ids
|
||||
.clone()
|
||||
.map(|id| id.map_or(RevisionTarget::Working, RevisionTarget::Commit));
|
||||
let resolved = if mode == ComparisonMode::BranchChanges
|
||||
&& before.target != RevisionTarget::Working
|
||||
&& after.target != RevisionTarget::Working
|
||||
{
|
||||
let (_, before_id) = self.resolve(&before.target)?;
|
||||
let (_, after_id) = self.resolve(&after.target)?;
|
||||
let base = self.merge_base(&before_id, &after_id)?;
|
||||
let base = self.merge_base(
|
||||
selected_ids[0].as_deref().unwrap(),
|
||||
selected_ids[1].as_deref().unwrap(),
|
||||
)?;
|
||||
let label = format!(
|
||||
"merge base of {} and {} · {base}",
|
||||
before.label, after.label
|
||||
);
|
||||
Some((
|
||||
RevisionTarget::Commit(base),
|
||||
RevisionTarget::Commit(after_id),
|
||||
label,
|
||||
))
|
||||
Some((RevisionTarget::Commit(base), targets[1].clone(), label))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (before_target, after_target, before_label) = resolved.as_ref().map_or(
|
||||
(&before.target, &after.target, before.label.as_str()),
|
||||
(&targets[0], &targets[1], before.label.as_str()),
|
||||
|(before, after, label)| (before, after, label.as_str()),
|
||||
);
|
||||
let working_after = *after_target == RevisionTarget::Working;
|
||||
@@ -2360,7 +2443,9 @@ impl Repository {
|
||||
let editable_after =
|
||||
working_after || (pinned_checkout.is_some() && pinned_checkout == checkout);
|
||||
let left = self.snapshot(before_target)?;
|
||||
let same = before_target == after_target && (!working_after || buffers.is_empty());
|
||||
let same = bookmarks.iter().all(Option::is_none)
|
||||
&& before_target == after_target
|
||||
&& (!working_after || buffers.is_empty());
|
||||
let right = if same {
|
||||
BTreeMap::new()
|
||||
} else if working_after {
|
||||
@@ -2371,6 +2456,9 @@ impl Repository {
|
||||
};
|
||||
let mut prepared = PreparedComparison {
|
||||
id: comparison_id,
|
||||
selected: [before.clone(), after.clone()],
|
||||
selected_ids,
|
||||
mode,
|
||||
left,
|
||||
right,
|
||||
files: vec![],
|
||||
@@ -2386,19 +2474,22 @@ impl Repository {
|
||||
if self.working_anchor()? != checkout {
|
||||
return Err("Checkout moved during comparison".into());
|
||||
}
|
||||
if self.bookmark_ids([&before.target, &after.target])? != bookmarks {
|
||||
return Err("Bookmark moved during comparison".into());
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
Ok(comparison)
|
||||
}
|
||||
|
||||
/// Follow Working forward when enabled, or update a pinned After's eligibility.
|
||||
/// Pinned sources and path catalogs never change when checkout eligibility does.
|
||||
/// Resolve active bookmarks, follow Working forward when enabled, or update
|
||||
/// pinned After eligibility without changing its frozen contents.
|
||||
pub fn refresh_checkout(
|
||||
&mut self,
|
||||
comparison_id: u64,
|
||||
new_id: u64,
|
||||
follow_working: bool,
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Option<Comparison>, String> {
|
||||
) -> Result<Option<CheckoutRefresh>, String> {
|
||||
let result = self.refresh_checkout_inner(comparison_id, new_id, follow_working, buffers);
|
||||
if result.is_err()
|
||||
&& let Some(prepared) = self.comparison.as_mut()
|
||||
@@ -2418,7 +2509,7 @@ impl Repository {
|
||||
new_id: u64,
|
||||
follow_working: bool,
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Option<Comparison>, String> {
|
||||
) -> Result<Option<CheckoutRefresh>, String> {
|
||||
let Some(prepared) = self
|
||||
.comparison
|
||||
.as_ref()
|
||||
@@ -2427,6 +2518,17 @@ impl Repository {
|
||||
return Ok(None);
|
||||
};
|
||||
self.check_cancelled()?;
|
||||
let bookmarks =
|
||||
self.bookmark_ids([&prepared.selected[0].target, &prepared.selected[1].target])?;
|
||||
if bookmarks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(index, id)| id.is_some() && id != &prepared.selected_ids[index])
|
||||
{
|
||||
return self
|
||||
.refresh_bookmarks(new_id, bookmarks, follow_working, buffers)
|
||||
.map(|comparison| Some(CheckoutRefresh::Bookmark(comparison)));
|
||||
}
|
||||
if prepared.working_after && !follow_working {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -2454,13 +2556,19 @@ impl Repository {
|
||||
if self.working_anchor()? != anchor {
|
||||
return Err("Checkout moved during comparison".into());
|
||||
}
|
||||
if self
|
||||
.bookmark_ids([&prepared.selected[0].target, &prepared.selected[1].target])?
|
||||
!= bookmarks
|
||||
{
|
||||
return Err("Bookmark moved during comparison".into());
|
||||
}
|
||||
Ok(comparison)
|
||||
});
|
||||
if result.is_ok() {
|
||||
prepared.id = new_id;
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
return result.map(Some);
|
||||
return result.map(|comparison| Some(CheckoutRefresh::Checkout(comparison)));
|
||||
}
|
||||
if !self.working_advanced(prepared.working_anchor.as_deref(), anchor.as_deref())? {
|
||||
return Ok(None);
|
||||
@@ -2476,6 +2584,12 @@ impl Repository {
|
||||
if self.working_anchor()? != anchor {
|
||||
return Err("Working checkout moved during comparison".into());
|
||||
}
|
||||
if self
|
||||
.bookmark_ids([&prepared.selected[0].target, &prepared.selected[1].target])?
|
||||
!= bookmarks
|
||||
{
|
||||
return Err("Bookmark moved during comparison".into());
|
||||
}
|
||||
Ok(comparison)
|
||||
});
|
||||
if result.is_ok() {
|
||||
@@ -2486,7 +2600,102 @@ impl Repository {
|
||||
prepared.files = previous_files;
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
result.map(Some)
|
||||
result.map(|comparison| Some(CheckoutRefresh::Checkout(comparison)))
|
||||
}
|
||||
|
||||
fn refresh_bookmarks(
|
||||
&mut self,
|
||||
new_id: u64,
|
||||
bookmarks: [Option<String>; 2],
|
||||
follow_working: bool,
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Comparison, String> {
|
||||
let prepared = self.comparison.as_ref().unwrap();
|
||||
let mut ids = prepared.selected_ids.clone();
|
||||
for (index, id) in bookmarks.iter().enumerate() {
|
||||
if id.is_some() {
|
||||
ids[index] = id.clone();
|
||||
}
|
||||
}
|
||||
let branch =
|
||||
prepared.mode == ComparisonMode::BranchChanges && ids.iter().all(Option::is_some);
|
||||
let label = prepared.selected[0].label.clone();
|
||||
let after_label = prepared.selected[1].label.clone();
|
||||
let working_after = prepared.working_after;
|
||||
let changed_before = ids[0] != prepared.selected_ids[0];
|
||||
let changed_after = ids[1] != prepared.selected_ids[1];
|
||||
let anchor = self.working_anchor()?;
|
||||
let advance_working = working_after
|
||||
&& follow_working
|
||||
&& self.working_advanced(prepared.working_anchor.as_deref(), anchor.as_deref())?;
|
||||
let (left, before_label) = if branch {
|
||||
let base = self.merge_base(ids[0].as_deref().unwrap(), ids[1].as_deref().unwrap())?;
|
||||
let label = format!("merge base of {label} and {after_label} · {base}");
|
||||
(Some(self.snapshot(&RevisionTarget::Commit(base))?), label)
|
||||
} else if changed_before {
|
||||
(
|
||||
Some(self.snapshot(&RevisionTarget::Commit(ids[0].clone().unwrap()))?),
|
||||
label,
|
||||
)
|
||||
} else {
|
||||
(None, label)
|
||||
};
|
||||
let right = if advance_working {
|
||||
Some(self.working_snapshot(buffers)?)
|
||||
} else if changed_after {
|
||||
Some(self.snapshot(&RevisionTarget::Commit(ids[1].clone().unwrap()))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pinned_checkout = ids[1]
|
||||
.as_deref()
|
||||
.map(|id| self.pinned_checkout(id))
|
||||
.transpose()?
|
||||
.flatten();
|
||||
let mut prepared = self.comparison.take().unwrap();
|
||||
let previous_left = left.map(|left| std::mem::replace(&mut prepared.left, left));
|
||||
let previous_right = right.map(|right| std::mem::replace(&mut prepared.right, right));
|
||||
let previous_files = std::mem::take(&mut prepared.files);
|
||||
let previous_before = std::mem::replace(&mut prepared.before, before_label);
|
||||
let previous_editable = prepared.editable_after;
|
||||
prepared.editable_after =
|
||||
working_after || (pinned_checkout.is_some() && pinned_checkout == anchor);
|
||||
let result = self
|
||||
.build_comparison(&mut prepared, false)
|
||||
.and_then(|comparison| {
|
||||
self.check_cancelled()?;
|
||||
if self.working_anchor()? != anchor {
|
||||
return Err("Checkout moved during comparison".into());
|
||||
}
|
||||
if self
|
||||
.bookmark_ids([&prepared.selected[0].target, &prepared.selected[1].target])?
|
||||
!= bookmarks
|
||||
{
|
||||
return Err("Bookmark moved during comparison".into());
|
||||
}
|
||||
Ok(comparison)
|
||||
});
|
||||
if result.is_ok() {
|
||||
prepared.id = new_id;
|
||||
prepared.selected_ids = ids;
|
||||
prepared.pinned_commit = prepared.selected_ids[1].clone();
|
||||
prepared.pinned_checkout = pinned_checkout;
|
||||
if advance_working {
|
||||
prepared.working_anchor = anchor;
|
||||
}
|
||||
} else {
|
||||
if let Some(left) = previous_left {
|
||||
prepared.left = left;
|
||||
}
|
||||
if let Some(right) = previous_right {
|
||||
prepared.right = right;
|
||||
}
|
||||
prepared.files = previous_files;
|
||||
prepared.before = previous_before;
|
||||
prepared.editable_after = previous_editable;
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
result
|
||||
}
|
||||
|
||||
fn build_comparison(
|
||||
|
||||
Reference in New Issue
Block a user