feat: refresh diffs when selected jj bookmarks move

This commit is contained in:
Valdemar Lorenzen
2026-10-15 15:18:13 +02:00
parent 1a02502488
commit c32d4a7590
8 changed files with 991 additions and 62 deletions
+48 -16
View File
@@ -63,6 +63,10 @@ impl ComparisonMode {
pub enum RevisionTarget {
Working,
Commit(String),
Bookmark {
name: String,
remote: Option<String>,
},
Remote {
remote: String,
reference: String,
@@ -178,6 +182,7 @@ pub enum Outcome {
},
Comparison(Comparison),
CheckoutRefresh(Option<Comparison>),
BookmarkRefresh(Comparison),
File {
comparison_id: u64,
file_index: usize,
@@ -299,6 +304,7 @@ pub struct DiffState {
max_before: usize,
max_after: usize,
after_widths: Vec<usize>,
pub(crate) refresh_view: Option<(usize, usize, usize)>,
}
impl DiffState {
@@ -355,6 +361,7 @@ impl DiffState {
max_before: 0,
max_after: 0,
after_widths: Vec::new(),
refresh_view: None,
}
}
pub(crate) fn set_resolver_paths(&mut self, root: PathBuf, paths: Vec<PathBuf>, file: usize) {
@@ -516,10 +523,15 @@ impl DiffState {
|| self.request_kind.is_some()
|| self.pending.is_some()
|| self.comparison.is_none()
|| self
.after
.as_ref()
.is_none_or(|after| after.target == RevisionTarget::Working && !follow_working)
|| self.after.as_ref().is_none_or(|after| {
after.target == RevisionTarget::Working
&& !follow_working
&& !self
.before
.iter()
.chain(self.after.iter())
.any(|revision| matches!(revision.target, RevisionTarget::Bookmark { .. }))
})
{
return;
}
@@ -635,18 +647,22 @@ impl DiffState {
}
self.request_kind = None;
let deferred = self.pending.take();
let bookmark_refresh = matches!(&result, Ok(Outcome::BookmarkRefresh(_)));
match result {
Ok(Outcome::CheckoutRefresh(None)) => {
let recovered = self.error.take().is_some();
if let Some(operation) = deferred {
self.queue(operation);
}
return false;
return recovered;
}
Ok(Outcome::CheckoutRefresh(Some(mut comparison))) => {
let pinned = self
.after
.as_ref()
.is_some_and(|after| after.target != RevisionTarget::Working);
Ok(Outcome::CheckoutRefresh(Some(mut comparison)))
| Ok(Outcome::BookmarkRefresh(mut comparison)) => {
let pinned = !bookmark_refresh
&& self
.after
.as_ref()
.is_some_and(|after| after.target != RevisionTarget::Working);
let view = (self.top, self.left_before, self.left_after);
if !pinned {
self.reviewed_files.clear();
@@ -690,12 +706,17 @@ impl DiffState {
self.live_files.clear();
self.live_buffer = None;
self.live_revision = None;
self.refresh_view = None;
self.follow_caret = false;
self.prepare_selected_file();
if pinned {
self.top = view.0.min(self.display_rows.len().saturating_sub(1));
self.left_before = view.1.min(self.max_before);
self.left_after = view.2.min(self.max_after);
if pinned || bookmark_refresh {
if self.selected_file_loaded() {
self.top = view.0.min(self.display_rows.len().saturating_sub(1));
self.left_before = view.1.min(self.max_before);
self.left_after = view.2.min(self.max_after);
} else {
self.refresh_view = Some(view);
}
}
self.normalize_focus();
}
@@ -713,7 +734,10 @@ impl DiffState {
}
return true;
}
if matches!(result, Ok(Outcome::CheckoutRefresh(_))) {
if matches!(
result,
Ok(Outcome::CheckoutRefresh(_) | Outcome::BookmarkRefresh(_))
) {
return false;
}
if let Ok(Outcome::File {
@@ -758,6 +782,7 @@ impl DiffState {
Ok(Outcome::Comparison(comparison)) if expected == Some(RequestKind::Comparison) => {
self.live_files.clear();
self.frozen_after.clear();
self.refresh_view = None;
self.reviewed_files.clear();
self.live_buffer = None;
self.live_revision = None;
@@ -811,6 +836,11 @@ impl DiffState {
*selected = file;
if self.file == file_index {
self.align_file();
if let Some(view) = self.refresh_view.take() {
self.top = view.0.min(self.display_rows.len().saturating_sub(1));
self.left_before = view.1.min(self.max_before);
self.left_after = view.2.min(self.max_after);
}
self.normalize_focus();
}
self.error = None;
@@ -1488,6 +1518,7 @@ impl DiffState {
return true;
}
self.file = index;
self.refresh_view = None;
self.normalize_focus();
self.live_buffer = None;
self.live_revision = None;
@@ -4267,7 +4298,8 @@ mod tests {
..
})
));
assert!(!state.apply_result(8, Ok(Outcome::CheckoutRefresh(None))));
assert!(state.apply_result(8, Ok(Outcome::CheckoutRefresh(None))));
assert!(state.error.is_none());
assert_eq!(state.comparison_id, Some(7));
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
+174 -16
View File
@@ -4188,16 +4188,13 @@ impl Editor {
{
Some(buffers)
}
Operation::RefreshCheckout {
follow_working: true,
buffers,
..
} if self
.diff
.as_ref()
.or(self.parked_diff.as_ref())
.and_then(|diff| diff.after.as_ref())
.is_some_and(|after| after.target == RevisionTarget::Working) =>
Operation::RefreshCheckout { buffers, .. }
if self
.diff
.as_ref()
.or(self.parked_diff.as_ref())
.and_then(|diff| diff.after.as_ref())
.is_some_and(|after| after.target == RevisionTarget::Working) =>
{
Some(buffers)
}
@@ -4275,11 +4272,14 @@ impl Editor {
}
let preserved_view = (self.diff.as_ref().is_some_and(|diff| {
diff.refresh_pending()
&& diff
.after
.as_ref()
.is_some_and(|after| after.target != RevisionTarget::Working)
}) && !matches!(&result, Ok(Outcome::CheckoutRefresh(None))))
&& (matches!(&result, Ok(Outcome::BookmarkRefresh(_)))
|| diff
.after
.as_ref()
.is_some_and(|after| after.target != RevisionTarget::Working))
&& !matches!(&result, Ok(Outcome::CheckoutRefresh(None)))
|| matches!(&result, Ok(Outcome::File { .. })) && diff.refresh_view.is_some()
}))
.then(|| self.active.view.inherit());
let Some(diff) = &mut self.diff else {
return false;
@@ -4288,11 +4288,14 @@ impl Editor {
return false;
}
let refresh = diff.refresh_pending();
let previous_comparison = diff.comparison_id;
if !diff.apply_result(id, result) {
return false;
}
if refresh {
self.pending_lsp_navigation = None;
if diff.comparison_id != previous_comparison {
self.pending_lsp_navigation = None;
}
self.diff_bound = None;
} else if let Some(files) = &mut diff.files {
files.set_view(self.file_explorer_view);
@@ -15573,6 +15576,161 @@ mod tests {
});
}
#[test]
fn bookmark_refresh_reloads_paths_and_preserves_view_until_lazy_content_arrives() {
use crate::diff::{ChangedFile, Comparison, FileStatus};
let directory = Directory::new();
let mut editor = editor();
historical_diff(
&mut editor,
&directory.0,
vec!["old before".into(); 4],
vec!["old after".into(); 4],
);
editor.diff.as_mut().unwrap().after.as_mut().unwrap().target = RevisionTarget::Bookmark {
name: "topic".into(),
remote: None,
};
editor.diff.as_mut().unwrap().focus = Focus::After;
editor.bind_diff_buffer();
editor.handle(Input::Char('j'));
editor.diff.as_mut().unwrap().top = 2;
editor.diff.as_mut().unwrap().left_after = 3;
editor.diff.as_mut().unwrap().reviewed_files.insert(0);
editor.diff.as_mut().unwrap().view_mode = ViewMode::Unified;
let old_comparison = editor.diff().unwrap().comparison_id.unwrap();
let old_snapshot = editor.buffer_id();
let cursor = editor.cursor();
let file = |path: &str, loaded| ChangedFile {
path: path.into(),
old_path: None,
status: FileStatus::Modified,
loaded,
before: if loaded {
vec!["new before".into(); 3]
} else {
vec![]
},
after: if loaded {
vec!["new after".into(); 3]
} else {
vec![]
},
before_eof: false,
after_eof: false,
hunks: if loaded {
crate::diff::line_hunks(
&vec!["new before".into(); 3],
false,
&vec!["new after".into(); 3],
false,
)
} else {
vec![]
},
notice: None,
};
editor.queue_checkout_refreshes(false);
let (_, refresh, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(
refresh,
Ok(Outcome::BookmarkRefresh(Comparison {
root: directory.0.clone(),
before: "base".into(),
after: "topic".into(),
editable_paths: Default::default(),
files: vec![file("a-new.txt", false), file("file.txt", false)],
}))
));
let diff = editor.diff().unwrap();
assert_eq!(diff.file, 1);
assert_eq!(diff.focus, Focus::After);
assert_eq!(diff.view_mode, ViewMode::Unified);
assert!(diff.reviewed_files.is_empty());
assert!(!diff.selected_file_loaded());
assert!(!diff.comparison.as_ref().unwrap().files[0].loaded);
assert_eq!(editor.buffer_id(), old_snapshot);
let (_, load, operation) = editor.take_next_diff_request().unwrap();
assert!(
matches!(operation, Operation::LoadFile { comparison_id, file_index: 1 } if comparison_id == refresh)
);
assert!(editor.apply_diff_result(
load,
Ok(Outcome::File {
comparison_id: refresh,
file_index: 1,
file: file("file.txt", true),
})
));
assert_eq!(editor.lines(), ["new after"; 3]);
assert_eq!(editor.cursor(), cursor);
assert_eq!(editor.diff().unwrap().top, 2);
assert_eq!(editor.diff().unwrap().left_after, 3);
assert_ne!(editor.buffer_id(), old_snapshot);
assert!(editor.mutation_blocked());
assert!(!editor.apply_diff_result(refresh, Ok(Outcome::CheckoutRefresh(None))));
assert_ne!(editor.diff().unwrap().comparison_id, Some(old_comparison));
}
#[test]
fn bookmark_refresh_errors_recover_and_hidden_replacements_preserve_dirty_buffers() {
let directory = Directory::new();
let path = directory.path("file.txt");
fs::write(&path, "original").unwrap();
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
working_diff(&mut editor, &directory.0, &["file.txt"]);
editor.diff.as_mut().unwrap().after.as_mut().unwrap().target = RevisionTarget::Bookmark {
name: "topic".into(),
remote: None,
};
editor.diff.as_mut().unwrap().focus = Focus::After;
editor.bind_diff_buffer();
let dirty_buffer = editor.buffer_id();
type_text(&mut editor, "ix");
editor.diff.as_mut().unwrap().reviewed_files.insert(0);
let comparison_id = editor.diff().unwrap().comparison_id;
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(request, Err("bookmark is conflicted".into())));
assert_eq!(editor.diff().unwrap().comparison_id, comparison_id);
assert_eq!(editor.lines(), ["original"]);
assert!(editor.diff().unwrap().reviewed_files.contains(&0));
assert_eq!(editor.buffer(dirty_buffer).unwrap().lines, ["xoriginal"]);
assert!(editor.buffer(dirty_buffer).unwrap().dirty());
editor.queue_checkout_refreshes(false);
let (_, recovery, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(recovery, Ok(Outcome::CheckoutRefresh(None))));
assert!(editor.diff().unwrap().error.is_none());
assert_eq!(editor.diff().unwrap().comparison_id, comparison_id);
assert!(editor.diff().unwrap().reviewed_files.contains(&0));
let hidden_tab = editor.tab_id();
let owner = editor.pane_id();
let old_snapshot = editor.buffer_id();
editor.execute("tab.new");
let visible_tab = editor.tab_id();
editor.queue_checkout_refreshes(false);
let (tab, request, _) = editor.take_next_diff_request().unwrap();
assert_eq!(tab, hidden_tab);
let mut replacement = editor.with_pane(owner, |editor| {
editor.diff.as_mut().unwrap().comparison.take().unwrap()
});
replacement.files[0].after = vec!["moved bookmark".into()];
assert!(editor.apply_diff_result(request, Ok(Outcome::BookmarkRefresh(replacement))));
assert_eq!(editor.tab_id(), visible_tab);
assert_eq!(editor.buffer_id(), old_snapshot);
assert_eq!(editor.lines(), ["original"]);
assert!(editor.mutation_blocked());
assert_eq!(editor.buffer(dirty_buffer).unwrap().lines, ["xoriginal"]);
assert!(editor.buffer(dirty_buffer).unwrap().dirty());
editor.with_pane(owner, |editor| {
assert_eq!(editor.lines(), ["moved bookmark"]);
assert!(editor.diff().unwrap().reviewed_files.is_empty());
assert!(editor.mutation_blocked());
});
assert_eq!(fs::read_to_string(&path).unwrap(), "original");
}
#[test]
fn completed_working_refresh_serializes_lazy_selection_and_invalidates_pending_navigation() {
for request_kind in [
+231 -22
View File
@@ -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(
+20 -5
View File
@@ -23,7 +23,7 @@ use crate::{
DocumentEvent, DocumentEventKind, DocumentSnapshot, PluginEvent, PluginUpdate,
TextPosition, UpdateEffect,
},
repository::Repository,
repository::{CheckoutRefresh, Repository},
ruby::{Config, RubyPlugin, load_config},
};
@@ -321,7 +321,15 @@ impl DiffWorker {
buffers,
} => repository
.refresh_checkout(comparison_id, id, follow_working, &buffers)
.map(DiffOutcome::CheckoutRefresh),
.map(|refresh| match refresh {
Some(CheckoutRefresh::Bookmark(comparison)) => {
DiffOutcome::BookmarkRefresh(comparison)
}
Some(CheckoutRefresh::Checkout(comparison)) => {
DiffOutcome::CheckoutRefresh(Some(comparison))
}
None => DiffOutcome::CheckoutRefresh(None),
}),
DiffOperation::LoadFile {
comparison_id,
file_index,
@@ -2177,7 +2185,7 @@ mod tests {
use crate::diff::{Catalog, Category, Comparison, RepoKind, Revision, RevisionTarget};
let fixture = Fixture::new();
fixture.file("buffer.txt", "base");
for working in [true, false] {
for (working, bookmark) in [(true, false), (false, false), (true, true), (false, true)] {
let mut session = fixture.open("");
session.editor.execute("diff");
let (id, _) = session.editor.take_diff_request().unwrap();
@@ -2188,7 +2196,14 @@ mod tests {
notice: None,
entries: [
RevisionTarget::Working,
RevisionTarget::Commit("base".into()),
if bookmark {
RevisionTarget::Bookmark {
name: "base".into(),
remote: None,
}
} else {
RevisionTarget::Commit("base".into())
},
]
.into_iter()
.map(|target| Revision {
@@ -2224,7 +2239,7 @@ mod tests {
assert!(session.editor.take_next_diff_request().is_none());
session.refresh_checkout_diffs_at(now + Duration::from_secs(tick));
let request = session.editor.take_next_diff_request();
assert_eq!(request.is_some(), !working || follow);
assert_eq!(request.is_some(), !working || follow || bookmark);
if let Some((_, refresh, operation)) = request {
assert!(
matches!(operation, DiffOperation::RefreshCheckout { comparison_id, follow_working, .. }