use std::path::Path;
use std::process::Command;
use crate::bookmark_updates::{BookmarkUpdate, UpdateType, parse_git_push_dry_run};
use crate::error::{JjHooksError, Result};
use crate::hooks::{HookOutcome, run_for_update};
use crate::jj::{self, JjCli};
use crate::runner::{Runner, Stage};
#[derive(Debug, Clone)]
pub struct PushReport {
pub per_bookmark: Vec<(BookmarkUpdate, HookOutcome)>,
pub skipped: bool,
}
impl PushReport {
pub fn any_failure(&self) -> bool {
self.per_bookmark.iter().any(|(_, o)| !o.success)
}
pub fn any_fixup(&self) -> bool {
self.per_bookmark
.iter()
.any(|(_, o)| o.fixup_commit.is_some())
}
}
pub fn run_checks(
jj: &JjCli,
workspace_root: &Path,
cli_runner: Option<Runner>,
stage: Stage,
push_args: &[String],
) -> Result<PushReport> {
let updates = dry_run_updates(jj, push_args)?;
if updates.is_empty() {
tracing::info!("nothing to push, skipping hooks");
return Ok(PushReport {
per_bookmark: vec![],
skipped: true,
});
}
let non_deletes: Vec<_> = updates
.into_iter()
.filter(|u| u.update_type != UpdateType::Delete)
.collect();
if non_deletes.is_empty() {
tracing::info!("only deletions to push, skipping hooks");
return Ok(PushReport {
per_bookmark: vec![],
skipped: true,
});
}
let primary_git_dir = jj::primary_git_dir(workspace_root)?;
let mut per_bookmark = Vec::with_capacity(non_deletes.len());
for update in non_deletes {
match cli_runner {
Some(r) => tracing::info!("{update}: running {} hooks", r.bin()),
None => tracing::info!("{update}: autodetecting runner inside target worktree"),
}
let outcome = run_for_update(jj, &primary_git_dir, cli_runner, stage, &update)?;
per_bookmark.push((update, outcome));
}
Ok(PushReport {
per_bookmark,
skipped: false,
})
}
pub fn maybe_advance_bookmarks(
jj: &JjCli,
report: &PushReport,
advance_bookmarks: bool,
) -> Result<Vec<String>> {
if !advance_bookmarks {
return Ok(vec![]);
}
let mut advanced = vec![];
for (update, outcome) in &report.per_bookmark {
if let Some(commit) = &outcome.fixup_commit {
let argv = advance_bookmark_argv(&update.bookmark, commit);
let argv: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
jj.run(&argv)?;
advanced.push(update.bookmark.clone());
}
}
Ok(advanced)
}
pub(crate) fn advance_bookmark_argv(bookmark: &str, commit: &str) -> Vec<String> {
vec![
"bookmark".into(),
"set".into(),
bookmark.into(),
"-r".into(),
commit.into(),
"--allow-backwards".into(),
"--ignore-working-copy".into(),
]
}
fn dry_run_updates(
jj: &JjCli,
push_args: &[String],
) -> Result<std::collections::HashSet<BookmarkUpdate>> {
let args = dry_run_argv(push_args);
let argv: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let output = jj.run_capture_stderr(&argv)?;
tracing::debug!("dry-run output:\n{output}");
parse_git_push_dry_run(&output)
}
pub(crate) fn dry_run_argv(push_args: &[String]) -> Vec<String> {
let mut args = vec![
"git".into(),
"push".into(),
"--dry-run".into(),
"--ignore-working-copy".into(),
];
args.extend(push_args.iter().cloned());
args
}
pub fn execute_push(jj: &JjCli, push_args: &[String], dry_run: bool) -> Result<()> {
let args = execute_push_argv(push_args, dry_run);
let argv: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let status = Command::new("jj")
.args(&argv)
.current_dir(jj.cwd())
.status()?;
if !status.success() {
return Err(JjHooksError::JjFailed {
status: status.code().unwrap_or(-1),
stderr: "jj git push failed".into(),
});
}
Ok(())
}
pub(crate) fn execute_push_argv(push_args: &[String], dry_run: bool) -> Vec<String> {
let mut args = vec!["git".into(), "push".into(), "--ignore-working-copy".into()];
args.extend(push_args.iter().cloned());
if dry_run {
args.push("--dry-run".into());
}
args
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execute_push_argv_includes_ignore_working_copy() {
let argv = execute_push_argv(&["-b".into(), "main".into()], false);
assert!(
argv.iter().any(|a| a == "--ignore-working-copy"),
"execute_push must pass --ignore-working-copy: {argv:?}"
);
assert_eq!(argv[0], "git");
assert_eq!(argv[1], "push");
}
#[test]
fn execute_push_argv_appends_dry_run_when_set() {
let argv = execute_push_argv(&[], true);
assert!(argv.iter().any(|a| a == "--dry-run"));
assert!(argv.iter().any(|a| a == "--ignore-working-copy"));
}
#[test]
fn execute_push_argv_passes_through_caller_args() {
let argv = execute_push_argv(
&[
"-b".into(),
"feature".into(),
"--allow-new".into(),
"--remote".into(),
"origin".into(),
],
false,
);
for needle in ["-b", "feature", "--allow-new", "--remote", "origin"] {
assert!(
argv.iter().any(|a| a == needle),
"expected `{needle}` in argv: {argv:?}"
);
}
}
#[test]
fn dry_run_argv_includes_ignore_working_copy() {
let argv = dry_run_argv(&["-b".into(), "main".into()]);
assert!(
argv.iter().any(|a| a == "--ignore-working-copy"),
"dry-run argv must include --ignore-working-copy: {argv:?}"
);
assert!(argv.iter().any(|a| a == "--dry-run"));
}
#[test]
fn advance_bookmark_argv_includes_ignore_working_copy() {
let argv = advance_bookmark_argv("main", "deadbeef");
assert!(
argv.iter().any(|a| a == "--ignore-working-copy"),
"advance-bookmark argv must include --ignore-working-copy: {argv:?}"
);
assert_eq!(argv[0], "bookmark");
assert_eq!(argv[1], "set");
assert_eq!(argv[2], "main");
assert!(argv.iter().any(|a| a == "--allow-backwards"));
}
}