fix: preserve lazy snapshots for conflicted jj revisions
Conflicted revisions read every file through jj file show, which returns empty output for symlinks and causes repository-wide subprocess fanout. Keep ordinary Git blob sources and immutable identities; render only selected conflicted paths through jj. Verified all 905 tests and the original local bookmark comparison with 25 correct paths in 0.522s, plus actual terminal conflict viewing. Strict Clippy is blocked by existing diagnostics outside this change.
This commit is contained in:
+67
-61
@@ -1048,6 +1048,8 @@ struct FileData {
|
||||
mode: String,
|
||||
bytes: Vec<u8>,
|
||||
object: Option<(PathBuf, String)>,
|
||||
// Conflicts retain their Git blob identity, but selected contents come from jj.
|
||||
projection: Option<(PathBuf, String)>,
|
||||
notice: Option<String>,
|
||||
}
|
||||
type Snapshot = BTreeMap<PathBuf, FileData>;
|
||||
@@ -1766,65 +1768,29 @@ impl Repository {
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn conflicted_snapshot(&self, root: &Path, id: &str) -> Result<Snapshot, String> {
|
||||
fn conflicted_snapshot(&self, root: &Path, store: &Path, id: &str) -> Result<Snapshot, String> {
|
||||
let revset = format!("commit_id({})", serde_json::to_string(id).unwrap());
|
||||
let template = r#""[" ++ json(path) ++ "," ++ json(file_type) ++ "," ++ json(executable) ++ "," ++ json(conflict) ++ "]\n""#;
|
||||
let template = r#""[" ++ json(path) ++ "," ++ json(conflict) ++ "]\n""#;
|
||||
let entries = text(self.jj(root, &["file", "list", "-r", &revset, "-T", template])?)?;
|
||||
let mut blobs = self.git_snapshot(store, id)?;
|
||||
let mut snapshot = BTreeMap::new();
|
||||
let mut total = 0;
|
||||
for entry in entries.lines() {
|
||||
self.check_cancelled()?;
|
||||
let (path, kind, executable, conflict): (String, String, bool, bool) =
|
||||
serde_json::from_str(entry)
|
||||
.map_err(|e| format!("Cannot decode Jujutsu conflict entry: {e}"))?;
|
||||
let path_buf = PathBuf::from(&path);
|
||||
safe_path(&path_buf)?;
|
||||
let mut notice = conflict.then(|| {
|
||||
"Unresolved Jujutsu conflict: showing CLI-rendered conflict projection".to_owned()
|
||||
});
|
||||
let mode = if kind == "symlink" {
|
||||
"120000"
|
||||
} else if kind == "git-submodule" {
|
||||
"160000"
|
||||
} else if executable {
|
||||
"100755"
|
||||
} else {
|
||||
"100644"
|
||||
};
|
||||
let bytes = if matches!(kind.as_str(), "file" | "symlink" | "conflict") {
|
||||
// Use an exact fileset and jj's own conflict materialization.
|
||||
let pattern = jj_file_pattern(&path);
|
||||
self.jj(
|
||||
root,
|
||||
&["file", "show", "-r", &revset, "-T", "", "--", &pattern],
|
||||
)?
|
||||
} else {
|
||||
notice = Some(format!(
|
||||
"Unsupported {kind} content in a conflicted revision; nested contents are not traversed"
|
||||
));
|
||||
vec![]
|
||||
};
|
||||
if bytes.len() > FILE_LIMIT {
|
||||
return Err(format!(
|
||||
"{} exceeds the 16 MiB comparison file limit",
|
||||
path_buf.display()
|
||||
));
|
||||
}
|
||||
total += bytes.len();
|
||||
if total > OUTPUT_LIMIT {
|
||||
return Err(
|
||||
"Jujutsu conflict projection exceeds the 64 MiB comparison limit".into(),
|
||||
let (path, conflict): (String, bool) = serde_json::from_str(entry)
|
||||
.map_err(|e| format!("Cannot decode Jujutsu conflict entry: {e}"))?;
|
||||
let path = PathBuf::from(path);
|
||||
safe_path(&path)?;
|
||||
// jj's logical file list excludes its private merge-tree encoding.
|
||||
let mut file = blobs
|
||||
.remove(&path)
|
||||
.ok_or_else(|| format!("Missing Jujutsu tree entry: {}", path.display()))?;
|
||||
if conflict {
|
||||
file.projection = Some((root.to_path_buf(), id.into()));
|
||||
file.notice = Some(
|
||||
"Unresolved Jujutsu conflict: showing CLI-rendered conflict projection".into(),
|
||||
);
|
||||
}
|
||||
snapshot.insert(
|
||||
path_buf,
|
||||
FileData {
|
||||
mode: mode.into(),
|
||||
bytes,
|
||||
object: None,
|
||||
notice,
|
||||
},
|
||||
);
|
||||
snapshot.insert(path, file);
|
||||
}
|
||||
Ok(snapshot)
|
||||
}
|
||||
@@ -1851,9 +1817,13 @@ impl Repository {
|
||||
],
|
||||
)?)?;
|
||||
if conflict.trim() == "true" {
|
||||
return self.conflicted_snapshot(root, id);
|
||||
return self.conflicted_snapshot(root, store, id);
|
||||
}
|
||||
}
|
||||
self.git_snapshot(store, id)
|
||||
}
|
||||
|
||||
fn git_snapshot(&self, store: &Path, id: &str) -> Result<Snapshot, String> {
|
||||
let output = self.git(store, &["ls-tree", "-r", "-z", "--full-tree", id])?;
|
||||
let mut snapshot = BTreeMap::new();
|
||||
for record in output.split(|b| *b == 0).filter(|r| !r.is_empty()) {
|
||||
@@ -1888,6 +1858,7 @@ impl Repository {
|
||||
mode: fields[0].into(),
|
||||
bytes: vec![],
|
||||
object,
|
||||
projection: None,
|
||||
notice,
|
||||
},
|
||||
);
|
||||
@@ -1898,6 +1869,7 @@ impl Repository {
|
||||
fn materialize(&self, snapshot: &mut Snapshot) -> Result<(), String> {
|
||||
let stores: BTreeSet<_> = snapshot
|
||||
.values()
|
||||
.filter(|file| file.projection.is_none())
|
||||
.filter_map(|file| file.object.as_ref().map(|(store, _)| store.clone()))
|
||||
.collect();
|
||||
for store in stores {
|
||||
@@ -1905,6 +1877,7 @@ impl Repository {
|
||||
for file in snapshot.values() {
|
||||
if let Some((source, id)) = &file.object
|
||||
&& source == &store
|
||||
&& file.projection.is_none()
|
||||
{
|
||||
request.extend_from_slice(id.as_bytes());
|
||||
request.push(b'\n');
|
||||
@@ -1915,10 +1888,11 @@ impl Repository {
|
||||
let data = checked(run(cmd, request, &self.cancelled)?)?;
|
||||
let mut offset = 0;
|
||||
for (path, file) in snapshot.iter_mut() {
|
||||
if file
|
||||
.object
|
||||
.as_ref()
|
||||
.is_none_or(|(source, _)| source != &store)
|
||||
if file.projection.is_some()
|
||||
|| file
|
||||
.object
|
||||
.as_ref()
|
||||
.is_none_or(|(source, _)| source != &store)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1951,6 +1925,33 @@ impl Repository {
|
||||
offset += size + 1;
|
||||
}
|
||||
}
|
||||
let mut total = 0;
|
||||
for (path, file) in snapshot.iter_mut() {
|
||||
let Some((root, id)) = &file.projection else {
|
||||
continue;
|
||||
};
|
||||
let revset = format!("commit_id({})", serde_json::to_string(id).unwrap());
|
||||
let pattern = jj_file_pattern(path.to_str().ok_or("Non-UTF-8 Jujutsu conflict path")?);
|
||||
let bytes = self.jj(
|
||||
root,
|
||||
&["file", "show", "-r", &revset, "-T", "", "--", &pattern],
|
||||
)?;
|
||||
if bytes.len() > FILE_LIMIT {
|
||||
return Err(format!(
|
||||
"{} exceeds the 16 MiB comparison file limit",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
total += bytes.len();
|
||||
if total > OUTPUT_LIMIT {
|
||||
return Err(
|
||||
"Jujutsu conflict projection exceeds the 64 MiB comparison limit".into(),
|
||||
);
|
||||
}
|
||||
file.bytes = bytes;
|
||||
file.object = None;
|
||||
file.projection = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn working_snapshot(&self, buffers: &[(PathBuf, String)]) -> Result<Snapshot, String> {
|
||||
@@ -2120,6 +2121,7 @@ impl Repository {
|
||||
mode: "160000".into(),
|
||||
bytes: vec![],
|
||||
object: None,
|
||||
projection: None,
|
||||
notice: Some(notice),
|
||||
},
|
||||
);
|
||||
@@ -2199,6 +2201,7 @@ impl Repository {
|
||||
mode,
|
||||
bytes,
|
||||
object: None,
|
||||
projection: None,
|
||||
notice,
|
||||
},
|
||||
);
|
||||
@@ -2493,8 +2496,8 @@ impl Repository {
|
||||
) -> Result<Comparison, String> {
|
||||
let left = &mut prepared.left;
|
||||
let right = &mut prepared.right;
|
||||
// Frozen checkout/conflict bytes have no object identity. Compare them in a
|
||||
// single batch, while immutable tree/tree comparisons need only object IDs.
|
||||
// Frozen checkout bytes have no object identity. Compare them in a single
|
||||
// batch; immutable blobs and deferred conflict projections retain IDs.
|
||||
let frozen = |file: &FileData| {
|
||||
file.object.is_none() && matches!(file.mode.as_str(), "100644" | "100755" | "120000")
|
||||
};
|
||||
@@ -2627,10 +2630,12 @@ impl Repository {
|
||||
let mut right = BTreeMap::new();
|
||||
let before = prepared.left.get(before_path);
|
||||
let after = prepared.right.get(path);
|
||||
if let Some(data) = before.filter(|data| data.object.is_some()) {
|
||||
if let Some(data) = before.filter(|data| data.object.is_some() || data.projection.is_some())
|
||||
{
|
||||
left.insert(before_path.clone(), clone_file_data(data));
|
||||
}
|
||||
if let Some(data) = after.filter(|data| data.object.is_some()) {
|
||||
if let Some(data) = after.filter(|data| data.object.is_some() || data.projection.is_some())
|
||||
{
|
||||
right.insert(path.clone(), clone_file_data(data));
|
||||
}
|
||||
self.materialize(&mut left)?;
|
||||
@@ -2731,6 +2736,7 @@ fn clone_file_data(file: &FileData) -> FileData {
|
||||
mode: file.mode.clone(),
|
||||
bytes: file.bytes.clone(),
|
||||
object: file.object.clone(),
|
||||
projection: file.projection.clone(),
|
||||
notice: file.notice.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user