|
| 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, Ok, Result}; |
| 13 | +use arc_swap::ArcSwap; |
| 14 | + |
| 15 | +use crate::FileChange; |
| 16 | + |
| 17 | +pub(super) fn get_diff_base(file: &Path) -> Result<Vec<u8>> { |
| 18 | + let jj_root_dir = find_jj_root(file)?; |
| 19 | + |
| 20 | + // We extracted the `jj_root_dir` from the file itself, if stripping the prefix fails |
| 21 | + // something has gone very very wrong |
| 22 | + let file_rel_to_dot_jj = file |
| 23 | + .strip_prefix(jj_root_dir) |
| 24 | + .expect("failed to strip diff path from jj root dir"); |
| 25 | + |
| 26 | + let tmpfile = tempfile::NamedTempFile::with_prefix("helix-jj-diff-") |
| 27 | + .context("could not create tempfile to save jj diff base")?; |
| 28 | + let tmppath = tmpfile.path(); |
| 29 | + |
| 30 | + let copy_bin = if cfg!(windows) { "copy.exe" } else { "cp" }; |
| 31 | + |
| 32 | + let status = Command::new("jj") |
| 33 | + .arg("--repository") |
| 34 | + .arg(jj_root_dir) |
| 35 | + .args([ |
| 36 | + "--ignore-working-copy", |
| 37 | + "diff", |
| 38 | + "--revision", |
| 39 | + "@", |
| 40 | + "--config-toml", |
| 41 | + ]) |
| 42 | + // Copy the temporary file provided by jujutsu to a temporary path of our own, |
| 43 | + // because the `$left` directory is deleted when `jj` finishes executing. |
| 44 | + .arg(format!( |
| 45 | + "ui.diff.tool = ['{exe}', '$left/{base}', '{target}']", |
| 46 | + exe = copy_bin, |
| 47 | + base = file_rel_to_dot_jj.display(), |
| 48 | + // Where to copy the jujutsu-provided file |
| 49 | + target = tmppath.display(), |
| 50 | + )) |
| 51 | + // Restrict the diff to the current file |
| 52 | + .arg(file) |
| 53 | + .stdout(std::process::Stdio::null()) |
| 54 | + .stderr(std::process::Stdio::null()) |
| 55 | + .status() |
| 56 | + .context("failed to execute jj diff command")?; |
| 57 | + |
| 58 | + let use_jj_path = status.success() && std::fs::metadata(tmppath).map_or(false, |m| m.len() > 0); |
| 59 | + // If the copy call inside `jj diff` succeeded, the tempfile is the one containing the base |
| 60 | + // else it's just the original file (so no diff). We check for size since `jj` can return |
| 61 | + // 0-sized files when there are no diffs to present for the file. |
| 62 | + let diff_base_path = if use_jj_path { tmppath } else { file }; |
| 63 | + |
| 64 | + // If the command succeeded, it means we either copied the jujutsu base or the current file, |
| 65 | + // so there should always be something to read and compare to. |
| 66 | + std::fs::read(diff_base_path).context("could not read jj diff base from the target") |
| 67 | +} |
| 68 | + |
| 69 | +pub(super) fn get_current_head_name(file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> { |
| 70 | + let jj_root_dir = find_jj_root(file)?; |
| 71 | + |
| 72 | + // See <https://github.com/martinvonz/jj/blob/main/docs/templates.md> |
| 73 | + // |
| 74 | + // This will produce the following: |
| 75 | + // |
| 76 | + // - If there are no branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm` |
| 77 | + // - If there is a single branch: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master)` |
| 78 | + // - If there are 2+ branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master, jj-diffs)` |
| 79 | + // |
| 80 | + // Always using the long id makes it easy to share it with others, which would not be the |
| 81 | + // case for shorter ones: they could have a local change that renders it ambiguous. |
| 82 | + let template = r#"separate(" ", change_id, surround("(", ")", branches.join(", ")))"#; |
| 83 | + |
| 84 | + let out = Command::new("jj") |
| 85 | + .arg("--repository") |
| 86 | + .arg(jj_root_dir) |
| 87 | + .args([ |
| 88 | + "--ignore-working-copy", |
| 89 | + "log", |
| 90 | + "--color", |
| 91 | + "never", |
| 92 | + "--revisions", |
| 93 | + "@", // Only display the current revision |
| 94 | + "--no-graph", |
| 95 | + "--no-pager", |
| 96 | + "--template", |
| 97 | + template, |
| 98 | + ]) |
| 99 | + .output()?; |
| 100 | + |
| 101 | + if !out.status.success() { |
| 102 | + anyhow::bail!("jj log command executed but failed"); |
| 103 | + } |
| 104 | + |
| 105 | + let out = String::from_utf8(out.stdout)?; |
| 106 | + |
| 107 | + let rev = out |
| 108 | + .lines() |
| 109 | + .next() |
| 110 | + .context("should always find at least one line")?; |
| 111 | + |
| 112 | + Ok(Arc::new(ArcSwap::from_pointee(rev.into()))) |
| 113 | +} |
| 114 | + |
| 115 | +pub(super) fn for_each_changed_file( |
| 116 | + cwd: &Path, |
| 117 | + callback: impl Fn(Result<FileChange>) -> bool, |
| 118 | +) -> Result<()> { |
| 119 | + let jj_root_dir = find_jj_root(cwd)?; |
| 120 | + |
| 121 | + let out = Command::new("jj") |
| 122 | + .arg("--repository") |
| 123 | + .arg(jj_root_dir) |
| 124 | + .args([ |
| 125 | + "--ignore-working-copy", |
| 126 | + "log", |
| 127 | + "--color", |
| 128 | + "never", |
| 129 | + "--revisions", |
| 130 | + "@", // Only display the current revision |
| 131 | + "--no-graph", |
| 132 | + "--no-pager", |
| 133 | + "--template", |
| 134 | + "", |
| 135 | + "--types", |
| 136 | + ]) |
| 137 | + .arg(cwd) |
| 138 | + .output()?; |
| 139 | + |
| 140 | + if !out.status.success() { |
| 141 | + anyhow::bail!("jj log command executed but failed"); |
| 142 | + } |
| 143 | + |
| 144 | + let out = String::from_utf8(out.stdout)?; |
| 145 | + |
| 146 | + for line in out.lines() { |
| 147 | + let mut split = line.splitn(2, ' '); |
| 148 | + |
| 149 | + let Some(status) = split.next() else { continue; }; |
| 150 | + let Some(path) = split.next() else { continue; }; |
| 151 | + |
| 152 | + let Some(change) = status_to_change(status, path) else { continue }; |
| 153 | + |
| 154 | + if !callback(Ok(change)) { |
| 155 | + break; |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + Ok(()) |
| 160 | +} |
| 161 | + |
| 162 | +/// Move up until we find the repository's root |
| 163 | +fn find_jj_root(file: &Path) -> Result<&Path> { |
| 164 | + file.ancestors() |
| 165 | + .find(|p| p.join(".jj").exists()) |
| 166 | + .context("no .jj dir found in parents") |
| 167 | +} |
| 168 | + |
| 169 | +/// Associate a status to a `FileChange`. |
| 170 | +fn status_to_change(status: &str, path: &str) -> Option<FileChange> { |
| 171 | + // Syntax: <https://github.com/martinvonz/jj/blob/320f50e00fcbd0d3ce27feb1e14b8e36d76b658f/cli/src/diff_util.rs#L68> |
| 172 | + Some(match status { |
| 173 | + "FF" | "LL" | "CF" | "CL" | "FL" | "LF" => FileChange::Modified { path: path.into() }, |
| 174 | + "-F" | "-L" => FileChange::Untracked { path: path.into() }, |
| 175 | + "F-" | "L-" => FileChange::Deleted { path: path.into() }, |
| 176 | + "FC" | "LC" => FileChange::Conflict { path: path.into() }, |
| 177 | + // We ignore gitsubmodules here since they not interesting in the context of |
| 178 | + // a file editor. |
| 179 | + _ => return None, |
| 180 | + }) |
| 181 | +} |
| 182 | + |
| 183 | +#[cfg(test)] |
| 184 | +mod tests { |
| 185 | + use std::path::PathBuf; |
| 186 | + |
| 187 | + use super::*; |
| 188 | + |
| 189 | + #[test] |
| 190 | + fn test_status_to_change() { |
| 191 | + let p = "helix-vcs/src/lib.rs"; |
| 192 | + let pb = PathBuf::from(p); |
| 193 | + |
| 194 | + for s in ["FF", "LL", "CF", "CL", "FL", "LF"] { |
| 195 | + assert_eq!( |
| 196 | + status_to_change(s, p).unwrap(), |
| 197 | + FileChange::Modified { path: pb.clone() } |
| 198 | + ); |
| 199 | + } |
| 200 | + for s in ["-F", "-L"] { |
| 201 | + assert_eq!( |
| 202 | + status_to_change(s, p).unwrap(), |
| 203 | + FileChange::Untracked { path: pb.clone() } |
| 204 | + ); |
| 205 | + } |
| 206 | + for s in ["F-", "L-"] { |
| 207 | + assert_eq!( |
| 208 | + status_to_change(s, p).unwrap(), |
| 209 | + FileChange::Deleted { path: pb.clone() } |
| 210 | + ); |
| 211 | + } |
| 212 | + for s in ["FC", "LC"] { |
| 213 | + assert_eq!( |
| 214 | + status_to_change(s, p).unwrap(), |
| 215 | + FileChange::Conflict { path: pb.clone() } |
| 216 | + ); |
| 217 | + } |
| 218 | + for s in ["GG", "LG", "ARO", "", " ", " "] { |
| 219 | + assert_eq!(status_to_change(s, p), None); |
| 220 | + } |
| 221 | + } |
| 222 | +} |
0 commit comments