|
| 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 | + |
| 32 | + let copy_bin = if cfg!(windows) { "copy.exe" } else { "cp" }; |
| 33 | + |
| 34 | + let status = Command::new("jj") |
| 35 | + .arg("-R") |
| 36 | + .arg(jj_root_dir) |
| 37 | + .args(["diff", "--config-toml"]) |
| 38 | + // Copy the temporary file provided by jujutsu to a temporary path of our own, |
| 39 | + // because the `$left` directory is deleted when `jj` finishes executing. |
| 40 | + .arg(format!( |
| 41 | + "ui.diff.tool = ['{exe}', '$left/{base}', '{target}']", |
| 42 | + exe = copy_bin, |
| 43 | + base = file_rel_to_dot_jj.display(), |
| 44 | + // Where to copy the jujutsu-provided file |
| 45 | + target = tmpfile.path().display(), |
| 46 | + )) |
| 47 | + // Restrict the diff to the current file |
| 48 | + .arg(file) |
| 49 | + .stdout(std::process::Stdio::null()) |
| 50 | + .stderr(std::process::Stdio::null()) |
| 51 | + .status() |
| 52 | + .context("failed to execute jj diff command")?; |
| 53 | + |
| 54 | + // If the copy call inside `jj diff` succeeded, the tempfile is the one containing the base |
| 55 | + // else it's just the original file (so no diff) |
| 56 | + let diff_base_path = if status.success() { |
| 57 | + tmpfile.path() |
| 58 | + } else { |
| 59 | + file |
| 60 | + }; |
| 61 | + |
| 62 | + // If the command succeeded, it means we either copied the jujutsu base or the current file, |
| 63 | + // so there should always be something to read and compare to. |
| 64 | + std::fs::read(diff_base_path).context("could not read jj diff base from the target") |
| 65 | + } |
| 66 | + |
| 67 | + fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> { |
| 68 | + let jj_root_dir = find_jj_root(file)?; |
| 69 | + |
| 70 | + // See <https://github.com/martinvonz/jj/blob/main/docs/templates.md> |
| 71 | + // |
| 72 | + // This will produce the following: |
| 73 | + // |
| 74 | + // - If there are no branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm` |
| 75 | + // - If there is a single branch: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master)` |
| 76 | + // - If there are 2+ branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master, jj-diffs)` |
| 77 | + // |
| 78 | + // Always using the long id makes it easy to share it with others, which would not be the |
| 79 | + // case for shorter ones: they could have a local change that renders it ambiguous. |
| 80 | + let template = r#"separate(" ", change_id, surround("(", ")", branches.join(", ")))"#; |
| 81 | + |
| 82 | + let out = Command::new("jj") |
| 83 | + .arg("-R") |
| 84 | + .arg(jj_root_dir) |
| 85 | + .args([ |
| 86 | + "log", |
| 87 | + "--color", |
| 88 | + "never", |
| 89 | + "--revisions", |
| 90 | + "@", // Only display the current revision |
| 91 | + "--no-graph", |
| 92 | + "--template", |
| 93 | + template, |
| 94 | + ]) |
| 95 | + .output()?; |
| 96 | + |
| 97 | + if !out.status.success() { |
| 98 | + anyhow::bail!("jj log command executed but failed"); |
| 99 | + } |
| 100 | + |
| 101 | + let out = String::from_utf8(out.stdout)?; |
| 102 | + |
| 103 | + let rev = out |
| 104 | + .lines() |
| 105 | + .next() |
| 106 | + .context("should always find at least one line")?; |
| 107 | + |
| 108 | + Ok(Arc::new(ArcSwap::from_pointee(rev.into()))) |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +// Move up until we find the repository's root |
| 113 | +fn find_jj_root(file: &Path) -> Result<&Path> { |
| 114 | + file.ancestors() |
| 115 | + .find(|p| p.join(".jj").exists()) |
| 116 | + .context("no .jj dir found in parents") |
| 117 | +} |
0 commit comments