feat: track checkout identity for pinned diff editability
This commit is contained in:
+131
-25
@@ -311,6 +311,9 @@ struct PreparedComparison {
|
||||
after: String,
|
||||
working_after: bool,
|
||||
working_anchor: Option<String>,
|
||||
pinned_commit: Option<String>,
|
||||
pinned_checkout: Option<String>,
|
||||
editable_after: bool,
|
||||
}
|
||||
|
||||
pub struct Repository {
|
||||
@@ -933,7 +936,7 @@ impl Repository {
|
||||
"log",
|
||||
"--no-graph",
|
||||
"-r",
|
||||
&format!("commit_id({})", serde_json::to_string(id).unwrap()),
|
||||
&format!("present(commit_id({}))", serde_json::to_string(id).unwrap()),
|
||||
"-T",
|
||||
"conflict",
|
||||
],
|
||||
@@ -1335,6 +1338,19 @@ impl Repository {
|
||||
}
|
||||
Ok(base.into())
|
||||
}
|
||||
fn pinned_checkout(&self, id: &str) -> Result<Option<String>, String> {
|
||||
if self.kind == RepoKind::Git {
|
||||
return Ok(Some(id.into()));
|
||||
}
|
||||
// Private remote history need not exist in the active jj operation.
|
||||
let revset = format!("present(commit_id({}))", serde_json::to_string(id).unwrap());
|
||||
let identity = text(self.jj(
|
||||
&self.root,
|
||||
&["log", "--no-graph", "-r", &revset, "-T", "change_id"],
|
||||
)?)?;
|
||||
let identity = identity.trim();
|
||||
Ok((!identity.is_empty()).then(|| identity.into()))
|
||||
}
|
||||
fn working_anchor(&self) -> Result<Option<String>, String> {
|
||||
if self.kind == RepoKind::Jujutsu {
|
||||
return Ok(Some(
|
||||
@@ -1417,11 +1433,20 @@ impl Repository {
|
||||
|(before, after, label)| (before, after, label.as_str()),
|
||||
);
|
||||
let working_after = *after_target == RevisionTarget::Working;
|
||||
let working_anchor = if working_after {
|
||||
self.working_anchor()?
|
||||
} else {
|
||||
let checkout = self.working_anchor()?;
|
||||
let pinned = if working_after {
|
||||
None
|
||||
} else {
|
||||
let (store, id) = self.resolve(after_target)?;
|
||||
Some((store, id))
|
||||
};
|
||||
let pinned_checkout = pinned
|
||||
.as_ref()
|
||||
.map(|(_, id)| self.pinned_checkout(id))
|
||||
.transpose()?
|
||||
.flatten();
|
||||
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 right = if same {
|
||||
@@ -1429,7 +1454,8 @@ impl Repository {
|
||||
} else if working_after {
|
||||
self.working_snapshot(buffers)?
|
||||
} else {
|
||||
self.snapshot(after_target)?
|
||||
let (store, id) = pinned.as_ref().unwrap();
|
||||
self.commit_snapshot(store, id)?
|
||||
};
|
||||
let mut prepared = PreparedComparison {
|
||||
id: comparison_id,
|
||||
@@ -1439,31 +1465,91 @@ impl Repository {
|
||||
before: before_label.into(),
|
||||
after: after.label.clone(),
|
||||
working_after,
|
||||
working_anchor,
|
||||
working_anchor: working_after.then(|| checkout.clone()).flatten(),
|
||||
pinned_commit: pinned.map(|(_, id)| id),
|
||||
pinned_checkout,
|
||||
editable_after,
|
||||
};
|
||||
let comparison = self.build_comparison(&mut prepared, same)?;
|
||||
if working_after && self.working_anchor()? != prepared.working_anchor {
|
||||
return Err("Working checkout moved during comparison".into());
|
||||
if self.working_anchor()? != checkout {
|
||||
return Err("Checkout moved during comparison".into());
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
Ok(comparison)
|
||||
}
|
||||
|
||||
/// Advance only a selected Working After, keeping its original Before frozen.
|
||||
pub fn refresh_working_after(
|
||||
/// Follow Working forward when enabled, or update a pinned After's eligibility.
|
||||
/// Pinned sources and path catalogs never change when checkout eligibility does.
|
||||
pub fn refresh_checkout(
|
||||
&mut self,
|
||||
comparison_id: u64,
|
||||
new_id: u64,
|
||||
follow_working: bool,
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Option<Comparison>, 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()
|
||||
&& prepared.id == comparison_id
|
||||
&& !prepared.working_after
|
||||
{
|
||||
// The frontend revokes on error; a recovered checkout must emit a
|
||||
// fresh promotion even if its identity did not change meanwhile.
|
||||
prepared.editable_after = false;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn refresh_checkout_inner(
|
||||
&mut self,
|
||||
comparison_id: u64,
|
||||
new_id: u64,
|
||||
follow_working: bool,
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Option<Comparison>, String> {
|
||||
let Some(prepared) = self
|
||||
.comparison
|
||||
.as_ref()
|
||||
.filter(|prepared| prepared.id == comparison_id && prepared.working_after)
|
||||
.filter(|prepared| prepared.id == comparison_id)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.check_cancelled()?;
|
||||
if prepared.working_after && !follow_working {
|
||||
return Ok(None);
|
||||
}
|
||||
if prepared.pinned_checkout.is_none()
|
||||
&& let Some(id) = prepared.pinned_commit.as_deref()
|
||||
{
|
||||
// A privately loaded endpoint may later be imported into active jj.
|
||||
// Retry only its frozen commit ID, never the mutable remote alias.
|
||||
let identity = self.pinned_checkout(id)?;
|
||||
self.comparison.as_mut().unwrap().pinned_checkout = identity;
|
||||
}
|
||||
let prepared = self.comparison.as_ref().unwrap();
|
||||
let anchor = self.working_anchor()?;
|
||||
if !prepared.working_after {
|
||||
let editable = prepared.pinned_checkout.is_some() && prepared.pinned_checkout == anchor;
|
||||
if editable == prepared.editable_after {
|
||||
return Ok(None);
|
||||
}
|
||||
// No live contents enter the frozen comparison, including dirty
|
||||
// frontend buffer overlays.
|
||||
let mut prepared = self.comparison.take().unwrap();
|
||||
prepared.editable_after = editable;
|
||||
let comparison = self.comparison_catalog(&prepared);
|
||||
let result = self.check_cancelled().and_then(|()| {
|
||||
if self.working_anchor()? != anchor {
|
||||
return Err("Checkout moved during comparison".into());
|
||||
}
|
||||
Ok(comparison)
|
||||
});
|
||||
if result.is_ok() {
|
||||
prepared.id = new_id;
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
return result.map(Some);
|
||||
}
|
||||
if !self.working_advanced(prepared.working_anchor.as_deref(), anchor.as_deref())? {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -1560,35 +1646,55 @@ impl Repository {
|
||||
removed.insert(j);
|
||||
}
|
||||
}
|
||||
let files: Vec<_> = files
|
||||
prepared.files = files
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, f)| (!removed.contains(&i)).then_some(f))
|
||||
.filter_map(|(i, file)| {
|
||||
(!removed.contains(&i)).then_some((file.path, file.old_path, file.status))
|
||||
})
|
||||
.collect();
|
||||
let editable_paths = if prepared.working_after {
|
||||
files
|
||||
Ok(self.comparison_catalog(prepared))
|
||||
}
|
||||
|
||||
fn comparison_catalog(&self, prepared: &PreparedComparison) -> Comparison {
|
||||
let files = prepared
|
||||
.files
|
||||
.iter()
|
||||
.map(|(path, old_path, status)| ChangedFile {
|
||||
path: path.clone(),
|
||||
old_path: old_path.clone(),
|
||||
status: *status,
|
||||
loaded: false,
|
||||
before: vec![],
|
||||
after: vec![],
|
||||
before_eof: false,
|
||||
after_eof: false,
|
||||
hunks: vec![],
|
||||
notice: None,
|
||||
})
|
||||
.collect();
|
||||
let editable_paths = if prepared.editable_after {
|
||||
prepared
|
||||
.files
|
||||
.iter()
|
||||
.filter(|file| {
|
||||
right_view
|
||||
.get(&file.path)
|
||||
.filter(|(path, _, _)| {
|
||||
prepared
|
||||
.right
|
||||
.get(path)
|
||||
.is_some_and(|data| matches!(data.mode.as_str(), "100644" | "100755"))
|
||||
})
|
||||
.map(|file| file.path.clone())
|
||||
.map(|(path, _, _)| path.clone())
|
||||
.collect()
|
||||
} else {
|
||||
BTreeSet::new()
|
||||
};
|
||||
prepared.files = files
|
||||
.iter()
|
||||
.map(|file| (file.path.clone(), file.old_path.clone(), file.status))
|
||||
.collect();
|
||||
Ok(Comparison {
|
||||
Comparison {
|
||||
before: prepared.before.clone(),
|
||||
after: prepared.after.clone(),
|
||||
files,
|
||||
root: self.root.clone(),
|
||||
editable_paths,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare one catalog entry using its original comparison's frozen sources.
|
||||
|
||||
Reference in New Issue
Block a user