feat: refresh diffs when selected jj bookmarks move
This commit is contained in:
+48
-16
@@ -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
@@ -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
@@ -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
@@ -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, .. }
|
||||
|
||||
Reference in New Issue
Block a user