Skip to content

Commit 1a03328

Browse files
committed
feat: Add Jujutsu diff handling to Helix
1 parent 1d10878 commit 1a03328

File tree

4 files changed

+144
-2
lines changed

4 files changed

+144
-2
lines changed

helix-term/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ repository.workspace = true
1313
homepage.workspace = true
1414

1515
[features]
16-
default = ["git"]
16+
default = ["vcs"]
1717
unicode-lines = ["helix-core/unicode-lines"]
1818
integration = ["helix-event/integration_test"]
19+
20+
# All VCSes available for diffs in Helix
21+
vcs = ["git", "jujutsu"]
1922
git = ["helix-vcs/git"]
23+
jujutsu = ["helix-vcs/jujutsu"]
2024

2125
[[bin]]
2226
name = "hx"

helix-vcs/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ imara-diff = "0.1.5"
2424
anyhow = "1"
2525

2626
log = "0.4"
27+
# For `jujutsu`
28+
tempfile = { version = "3.10", optional = true }
2729

2830
[features]
2931
git = ["gix"]
32+
jujutsu = ["tempfile"]
33+
3034

3135
[dev-dependencies]
3236
tempfile = "3.10"

helix-vcs/src/jujutsu.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//! Jujutsu works with several backends and could add new ones in the future. Private builds of
2+
//! it could also have private backends. Those make it hard to use `jj-lib` since it won't have
3+
//! access to newer or private backends and fail to compute the diffs for them.
4+
//!
5+
//! Instead in case there *is* a diff to base ourselves on, we copy it to a tempfile or just use the
6+
//! current file if not.
7+
8+
use std::path::Path;
9+
use std::process::Command;
10+
use std::sync::Arc;
11+
12+
use anyhow::{Context, Result};
13+
use arc_swap::ArcSwap;
14+
15+
use crate::DiffProvider;
16+
17+
pub(super) struct Jujutsu;
18+
19+
impl DiffProvider for Jujutsu {
20+
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
21+
let jj_root_dir = find_jj_root(file)?;
22+
23+
// We extracted the `jj_root_dir` from the file itself, if stripping the prefix fails
24+
// something has gone very very wrong
25+
let file_rel_to_dot_jj = file
26+
.strip_prefix(jj_root_dir)
27+
.expect("failed to strip diff path from jj root dir");
28+
29+
let tmpfile = tempfile::NamedTempFile::with_prefix("helix-jj-diff-")
30+
.context("could not create tempfile to save jj diff base")?;
31+
let tmppath = tmpfile.path();
32+
33+
let copy_bin = if cfg!(windows) { "copy.exe" } else { "cp" };
34+
35+
let status = Command::new("jj")
36+
.arg("--repository")
37+
.arg(jj_root_dir)
38+
.args([
39+
"--ignore-working-copy",
40+
"diff",
41+
"--revision",
42+
"@",
43+
"--config-toml",
44+
])
45+
// Copy the temporary file provided by jujutsu to a temporary path of our own,
46+
// because the `$left` directory is deleted when `jj` finishes executing.
47+
.arg(format!(
48+
"ui.diff.tool = ['{exe}', '$left/{base}', '{target}']",
49+
exe = copy_bin,
50+
base = file_rel_to_dot_jj.display(),
51+
// Where to copy the jujutsu-provided file
52+
target = tmppath.display(),
53+
))
54+
// Restrict the diff to the current file
55+
.arg(file)
56+
.stdout(std::process::Stdio::null())
57+
.stderr(std::process::Stdio::null())
58+
.status()
59+
.context("failed to execute jj diff command")?;
60+
61+
let use_jj_path =
62+
status.success() && std::fs::metadata(tmppath).map_or(false, |m| m.len() > 0);
63+
// If the copy call inside `jj diff` succeeded, the tempfile is the one containing the base
64+
// else it's just the original file (so no diff). We check for size since `jj` can return
65+
// 0-sized files when there are no diffs to present for the file.
66+
let diff_base_path = if use_jj_path { tmppath } else { file };
67+
68+
// If the command succeeded, it means we either copied the jujutsu base or the current file,
69+
// so there should always be something to read and compare to.
70+
std::fs::read(diff_base_path).context("could not read jj diff base from the target")
71+
}
72+
73+
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
74+
let jj_root_dir = find_jj_root(file)?;
75+
76+
// See <https://github.com/martinvonz/jj/blob/main/docs/templates.md>
77+
//
78+
// This will produce the following:
79+
//
80+
// - If there are no branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm`
81+
// - If there is a single branch: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master)`
82+
// - If there are 2+ branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master, jj-diffs)`
83+
//
84+
// Always using the long id makes it easy to share it with others, which would not be the
85+
// case for shorter ones: they could have a local change that renders it ambiguous.
86+
let template = r#"separate(" ", change_id, surround("(", ")", branches.join(", ")))"#;
87+
88+
let out = Command::new("jj")
89+
.arg("--repository")
90+
.arg(jj_root_dir)
91+
.args([
92+
"--ignore-working-copy",
93+
"log",
94+
"--color",
95+
"never",
96+
"--revisions",
97+
"@", // Only display the current revision
98+
"--no-graph",
99+
"--template",
100+
template,
101+
])
102+
.output()?;
103+
104+
if !out.status.success() {
105+
anyhow::bail!("jj log command executed but failed");
106+
}
107+
108+
let out = String::from_utf8(out.stdout)?;
109+
110+
let rev = out
111+
.lines()
112+
.next()
113+
.context("should always find at least one line")?;
114+
115+
Ok(Arc::new(ArcSwap::from_pointee(rev.into())))
116+
}
117+
}
118+
119+
// Move up until we find the repository's root
120+
fn find_jj_root(file: &Path) -> Result<&Path> {
121+
file.ancestors()
122+
.find(|p| p.join(".jj").exists())
123+
.context("no .jj dir found in parents")
124+
}

helix-vcs/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ pub use Dummy as Git;
1010
#[cfg(feature = "git")]
1111
mod git;
1212

13+
#[cfg(feature = "jujutsu")]
14+
mod jujutsu;
15+
1316
mod diff;
1417

1518
pub use diff::{DiffHandle, Hunk};
@@ -72,7 +75,14 @@ impl Default for DiffProviderRegistry {
7275
// currently only git is supported
7376
// TODO make this configurable when more providers are added
7477
let git: Box<dyn DiffProvider> = Box::new(Git);
75-
let providers = vec![git];
78+
#[cfg(feature = "jujutsu")]
79+
let jj: Box<dyn DiffProvider> = Box::new(jujutsu::Jujutsu);
80+
81+
let providers = vec![
82+
git,
83+
#[cfg(feature = "jujutsu")]
84+
jj,
85+
];
7686
DiffProviderRegistry { providers }
7787
}
7888
}

0 commit comments

Comments
 (0)