use super::imp::{VcsRevision, cmd_label, do_run, do_run_bytes};
use anyhow::{Context, bail};
use camino::{Utf8Path, Utf8PathBuf};
use git_stub::GitCommitHash;
use std::process::Command;
pub(super) fn jj_merge_base_head(
repo_root: &Utf8Path,
revision: &VcsRevision,
) -> anyhow::Result<GitCommitHash> {
let mut cmd = jj_start(repo_root);
cmd.args([
"log",
"--revisions",
&format!("heads(::@ & ::({}))", revision),
"--template",
"commit_id ++ \"\\n\"",
"--no-graph",
]);
let stdout = do_run(&mut cmd)?;
let stdout = stdout.trim();
if stdout.is_empty() {
bail!(
"no merge base found between @ and {revision} \
(is the revision valid?)"
);
}
let mut lines = stdout.lines();
let first_line =
lines.next().expect("non-empty stdout has at least one line");
if lines.next().is_some() {
bail!(
"multiple merge bases found between @ and {revision} \
(criss-cross merge?)"
);
}
first_line.parse().with_context(|| {
format!(
"jj returned unexpected merge-base output {:?} \
(expected a commit hash)",
first_line,
)
})
}
pub(super) fn jj_is_ancestor(
repo_root: &Utf8Path,
potential_ancestor: GitCommitHash,
commit: GitCommitHash,
) -> anyhow::Result<bool> {
let mut cmd = jj_start(repo_root);
cmd.args([
"log",
"--revisions",
&format!("{potential_ancestor} & ::{commit}"),
"--template",
"commit_id",
"--no-graph",
]);
let stdout = do_run(&mut cmd)?;
let trimmed = stdout.trim();
if trimmed.is_empty() {
return Ok(false);
}
if !trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
bail!(
"unexpected output from jj ancestor check: {:?} \
(expected a commit ID or empty output)",
trimmed,
);
}
Ok(true)
}
pub(super) fn jj_list_files(
repo_root: &Utf8Path,
revision: GitCommitHash,
directory: &Utf8Path,
) -> anyhow::Result<Vec<Utf8PathBuf>> {
let mut cmd = jj_start(repo_root);
cmd.args([
"file",
"list",
"--revision",
&revision.to_string(),
"--",
directory.as_str(),
]);
let label = cmd_label(&cmd);
let stdout = do_run(&mut cmd)?;
stdout
.lines()
.filter(|line| !line.is_empty())
.map(|line| {
let found_path = Utf8PathBuf::from(line);
let Ok(relative) = found_path.strip_prefix(directory) else {
bail!(
"jj file list returned a path that did not start \
with {:?}: {:?} (cmd: {})",
directory,
found_path,
label,
);
};
Ok(relative.to_owned())
})
.collect::<Result<Vec<_>, _>>()
}
pub(super) fn jj_show_file(
repo_root: &Utf8Path,
revision: GitCommitHash,
path: &Utf8Path,
) -> anyhow::Result<Vec<u8>> {
let mut cmd = jj_start(repo_root);
cmd.args(["file", "show", "--revision", &revision.to_string(), "--"])
.arg(path);
do_run_bytes(&mut cmd)
}
pub(super) fn jj_first_commit_for_file(
repo_root: &Utf8Path,
revision: GitCommitHash,
path: &Utf8Path,
) -> anyhow::Result<GitCommitHash> {
let quoted_path = revset_quote(path.as_str());
let template = format!(
"if(self.diff({quoted_path}).files().any(\
|entry| entry.status_char() == \"A\" \
|| entry.status_char() == \"R\" \
|| entry.status_char() == \"C\"\
), commit_id ++ \"\\n\", \"\")",
);
let mut cmd = jj_start(repo_root);
cmd.args([
"log",
"--revisions",
&format!("files({quoted_path}) & ::{revision}"),
"--template",
&template,
"--no-graph",
]);
let stdout = do_run(&mut cmd)?;
let commit = stdout.trim();
let first_commit = commit.lines().next().with_context(|| {
format!(
"no commit found that added file {:?} \
(searched backwards from {})",
path, revision,
)
})?;
first_commit.parse().with_context(|| {
format!(
"jj returned invalid commit hash {:?} for {:?}",
first_commit, path
)
})
}
fn revset_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' | '\\' => {
out.push('\\');
out.push(c);
}
_ => out.push(c),
}
}
out.push('"');
out
}
fn jj_start(repo_root: &Utf8Path) -> Command {
let jj = std::env::var("JJ").ok().unwrap_or_else(|| String::from("jj"));
let mut command = Command::new(&jj);
command.current_dir(repo_root);
command.args(["--no-pager", "--color", "never", "--ignore-working-copy"]);
command
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_revset_quote() {
assert_eq!(revset_quote("trunk()"), r#""trunk()""#);
assert_eq!(revset_quote("main"), r#""main""#);
assert_eq!(revset_quote("my-feature"), r#""my-feature""#);
assert_eq!(revset_quote("path/to/file.json"), r#""path/to/file.json""#);
assert_eq!(
revset_quote(r#"they said "hello""#),
r#""they said \"hello\"""#
);
assert_eq!(revset_quote(r"path\to\file"), r#""path\\to\\file""#);
assert_eq!(revset_quote(r#"a\"b"#), r#""a\\\"b""#);
assert_eq!(revset_quote(""), r#""""#);
}
}