feat: default diffs to branch changes

This commit is contained in:
Valdemar Lorenzen
2026-10-13 10:41:37 +02:00
parent fafec2a6b0
commit 6c254f9b0d
10 changed files with 562 additions and 33 deletions
+88 -12
View File
@@ -1,7 +1,7 @@
//! Read-only active-repository discovery and isolated revision comparison.
use crate::diff::{
Catalog, Category, ChangedFile, Comparison, FileStatus, RepoKind, Revision, RevisionTarget,
line_hunks,
Catalog, Category, ChangedFile, Comparison, ComparisonMode, FileStatus, RepoKind, Revision,
RevisionTarget, line_hunks,
};
use std::{
collections::{BTreeMap, BTreeSet},
@@ -664,6 +664,27 @@ impl Repository {
checked(run(cmd, vec![], &self.cancelled)?)?;
self.private = Some((root.clone(), root));
}
// Only the private store gains alternates; active refs and objects stay untouched.
let objects = self.git(
&self.store,
&[
"rev-parse",
"--path-format=absolute",
"--git-path",
"objects",
],
)?;
let mut alternate = vec![b'"'];
for &byte in objects.strip_suffix(b"\n").unwrap_or(&objects) {
if (b' '..=b'~').contains(&byte) && !matches!(byte, b'"' | b'\\') {
alternate.push(byte);
} else {
write!(&mut alternate, "\\{byte:03o}").unwrap();
}
}
alternate.extend_from_slice(b"\"\n");
let store = &self.private.as_ref().unwrap().1;
fs::write(store.join("objects/info/alternates"), alternate).map_err(|e| e.to_string())?;
Ok(())
}
fn load_remote(&mut self, name: &str, reference: Option<&str>) -> Result<(), String> {
@@ -681,10 +702,7 @@ impl Repository {
let (root, store) = self.private.as_ref().unwrap();
if self.kind == RepoKind::Jujutsu {
if !self.registered.contains(name) {
self.jj(
root,
&["git", "remote", "add", "--fetch-tags=all", "--", name, &url],
)?;
self.jj(root, &["git", "remote", "add", "--", name, &url])?;
self.registered.insert(name.into());
}
self.jj(
@@ -1279,6 +1297,40 @@ impl Repository {
self.commit_snapshot(&store, &id)
}
}
fn merge_base(&mut self, before: &str, after: &str) -> Result<String, String> {
const JJ_ROOT: &str = "0000000000000000000000000000000000000000";
if self.kind == RepoKind::Jujutsu && (before == JJ_ROOT || after == JJ_ROOT) {
return Ok(JJ_ROOT.into());
}
let store = self
.private
.as_ref()
.map_or(&self.store, |(_, store)| store);
let mut command = git_command(store);
command.args(["merge-base", "--all", before, after]);
let output = run(command, vec![], &self.cancelled)?;
if output.code == Some(1) && output.stdout.is_empty() {
// Every jj history has the synthetic root, absent from Git's graph.
return if self.kind == RepoKind::Jujutsu {
Ok(JJ_ROOT.into())
} else {
Err(
"Branch changes: no merge base; use Snapshots to compare unrelated histories"
.into(),
)
};
}
let bases = text(checked(output).map_err(|e| format!("Cannot resolve merge base: {e}"))?)?;
let mut bases = bases.lines();
let base = bases.next().ok_or("Branch changes: no merge base")?;
if bases.next().is_some() {
return Err(
"Branch changes: multiple merge bases; select Snapshots or different endpoints"
.into(),
);
}
Ok(base.into())
}
/// Freeze comparison sources and return path/status metadata only.
/// A new comparison replaces sources retained for the previous request ID.
pub fn compare(
@@ -1286,19 +1338,43 @@ impl Repository {
comparison_id: u64,
before: &Revision,
after: &Revision,
mode: ComparisonMode,
buffers: &[(PathBuf, String)],
) -> Result<Comparison, String> {
self.comparison = None;
let mut left = self.snapshot(&before.target)?;
let working_after = after.target == RevisionTarget::Working;
let mut right = if before.target == after.target && (!working_after || buffers.is_empty()) {
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 label = format!(
"merge base of {} and {} · {base}",
before.label, after.label
);
Some((
RevisionTarget::Commit(base),
RevisionTarget::Commit(after_id),
label,
))
} else {
None
};
let (before_target, after_target, before_label) = resolved.as_ref().map_or(
(&before.target, &after.target, before.label.as_str()),
|(before, after, label)| (before, after, label.as_str()),
);
let mut left = self.snapshot(before_target)?;
let working_after = *after_target == RevisionTarget::Working;
let same = before_target == after_target && (!working_after || buffers.is_empty());
let mut right = if same {
BTreeMap::new()
} else if working_after {
self.working_snapshot(buffers)?
} else {
self.snapshot(&after.target)?
self.snapshot(after_target)?
};
let same = before.target == after.target && (!working_after || buffers.is_empty());
// Frozen checkout/conflict bytes have no object identity. Compare them in a
// single batch, while immutable tree/tree comparisons need only object IDs.
let frozen = |file: &FileData| {
@@ -1389,7 +1465,7 @@ impl Repository {
.collect(),
});
Ok(Comparison {
before: before.label.clone(),
before: before_label.into(),
after: after.label.clone(),
files,
root: self.root.clone(),