Skip to content

Commit

Permalink
feat: Add Jujutsu diff handling to Helix
Browse files Browse the repository at this point in the history
  • Loading branch information
poliorcetics committed Mar 24, 2024
1 parent 47995bf commit e36fdad
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 2 deletions.
6 changes: 5 additions & 1 deletion helix-term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ repository.workspace = true
homepage.workspace = true

[features]
default = ["git"]
default = ["vcs"]
unicode-lines = ["helix-core/unicode-lines"]
integration = ["helix-event/integration_test"]

# All VCSes available for diffs in Helix
vcs = ["git", "jujutsu"]
git = ["helix-vcs/git"]
jujutsu = ["helix-vcs/jujutsu"]

[[bin]]
name = "hx"
Expand Down
4 changes: 4 additions & 0 deletions helix-vcs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ imara-diff = "0.1.5"
anyhow = "1"

log = "0.4"
# For `jujutsu`
tempfile = { version = "3.10", optional = true }

[features]
git = ["gix"]
jujutsu = ["tempfile"]


[dev-dependencies]
tempfile = "3.10"
118 changes: 118 additions & 0 deletions helix-vcs/src/jujutsu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Jujutsu works with several backends and could add new ones in the future. Private builds of
//! it could also have private backends. Those make it hard to use `jj-lib` since it won't have
//! access to newer or private backends and fail to compute the diffs for them.
//!
//! Instead in case there *is* a diff to base ourselves on, we copy it to a tempfile or just use the
//! current file if not.
use std::path::Path;
use std::process::Command;
use std::sync::Arc;

use anyhow::{Context, Result};
use arc_swap::ArcSwap;

use crate::DiffProvider;

pub(super) struct Jujutsu;

impl DiffProvider for Jujutsu {
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
let jj_root_dir = find_jj_root(file)?;

// We extracted the `jj_root_dir` from the file itself, if stripping the prefix fails
// something has gone very very wrong
let file_rel_to_dot_jj = file
.strip_prefix(jj_root_dir)
.expect("failed to strip diff path from jj root dir");

let tmpfile = tempfile::NamedTempFile::with_prefix("helix-jj-diff-")
.context("could not create tempfile to save jj diff base")?;
let tmppath = tmpfile.path();

let copy_bin = if cfg!(windows) { "copy.exe" } else { "cp" };

let status = Command::new("jj")
.arg("--repository")
.arg(jj_root_dir)
.args(["diff", "--revision", "@", "--config-toml"])
// Copy the temporary file provided by jujutsu to a temporary path of our own,
// because the `$left` directory is deleted when `jj` finishes executing.
.arg(format!(
"ui.diff.tool = ['{exe}', '$left/{base}', '{target}']",
exe = copy_bin,
base = file_rel_to_dot_jj.display(),
// Where to copy the jujutsu-provided file
target = tmppath.display(),
))
// Restrict the diff to the current file
.arg(file)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.context("failed to execute jj diff command")?;

let use_jj_path =
status.success() && std::fs::metadata(tmppath).map_or(false, |m| m.len() > 0);
// If the copy call inside `jj diff` succeeded, the tempfile is the one containing the base
// else it's just the original file (so no diff). We check for size since `jj` can return
// 0-sized files when there are no diffs to present for the file.
let diff_base_path = if use_jj_path { tmppath } else { file };

// If the command succeeded, it means we either copied the jujutsu base or the current file,
// so there should always be something to read and compare to.
std::fs::read(diff_base_path).context("could not read jj diff base from the target")
}

fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
let jj_root_dir = find_jj_root(file)?;

// See <https://github.com/martinvonz/jj/blob/main/docs/templates.md>
//
// This will produce the following:
//
// - If there are no branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm`
// - If there is a single branch: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master)`
// - If there are 2+ branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master, jj-diffs)`
//
// Always using the long id makes it easy to share it with others, which would not be the
// case for shorter ones: they could have a local change that renders it ambiguous.
let template = r#"separate(" ", change_id, surround("(", ")", branches.join(", ")))"#;

let out = Command::new("jj")
.arg("--repository")
.arg(jj_root_dir)
.args([
"--ignore-working-copy",
"log",
"--color",
"never",
"--revisions",
"@", // Only display the current revision
"--no-graph",
"--template",
template,
])
.output()?;

if !out.status.success() {
anyhow::bail!("jj log command executed but failed");
}

let out = String::from_utf8(out.stdout)?;

let rev = out
.lines()
.next()
.context("should always find at least one line")?;

Ok(Arc::new(ArcSwap::from_pointee(rev.into())))
}
}

// Move up until we find the repository's root
fn find_jj_root(file: &Path) -> Result<&Path> {
file.ancestors()
.find(|p| p.join(".jj").exists())
.context("no .jj dir found in parents")
}
12 changes: 11 additions & 1 deletion helix-vcs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pub use Dummy as Git;
#[cfg(feature = "git")]
mod git;

#[cfg(feature = "jujutsu")]
mod jujutsu;

mod diff;

pub use diff::{DiffHandle, Hunk};
Expand Down Expand Up @@ -72,7 +75,14 @@ impl Default for DiffProviderRegistry {
// currently only git is supported
// TODO make this configurable when more providers are added
let git: Box<dyn DiffProvider> = Box::new(Git);
let providers = vec![git];
#[cfg(feature = "jujutsu")]
let jj: Box<dyn DiffProvider> = Box::new(jujutsu::Jujutsu);

let providers = vec![
git,
#[cfg(feature = "jujutsu")]
jj,
];
DiffProviderRegistry { providers }
}
}

0 comments on commit e36fdad

Please sign in to comment.