use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
use git2::{Oid, Repository};
#[derive(Debug, Clone)]
pub enum Selection {
Paths(Vec<PathBuf>),
All {
base: PathBuf,
},
}
pub(crate) fn resolve_selection(selection: &Selection) -> Result<Vec<PathBuf>> {
match selection {
Selection::Paths(paths) => Ok(paths.clone()),
Selection::All { base } => all_worktree_paths(base),
}
}
pub(crate) fn all_worktree_paths(base: &Path) -> Result<Vec<PathBuf>> {
let repo = Repository::discover(base)
.with_context(|| format!("not inside a git repository: {}", base.display()))?;
let root = main_root(&repo);
let main_repo = Repository::open(&root)
.with_context(|| format!("cannot open main repository: {}", root.display()))?;
let names = main_repo
.worktrees()
.context("cannot enumerate worktrees")?;
let mut paths = vec![root];
for name in names.iter().flatten().flatten() {
if let Ok(worktree) = main_repo.find_worktree(name) {
paths.push(worktree.path().to_path_buf());
}
}
Ok(paths)
}
pub(crate) fn main_root(repo: &Repository) -> PathBuf {
let commondir = repo.commondir();
let commondir = std::fs::canonicalize(commondir).unwrap_or_else(|_| commondir.to_path_buf());
let parent = commondir.parent().map(Path::to_path_buf);
parent.unwrap_or(commondir)
}
pub(crate) fn head_branch(repo: &Repository) -> (Option<String>, Option<Oid>) {
match repo.head() {
Ok(head) if head.is_branch() => (
head.shorthand().ok().map(ToString::to_string),
head.target(),
),
Ok(head) => (None, head.target()),
Err(_) => (None, None),
}
}
pub(crate) fn run_git_in(git: &Path, dir: &Path, args: &[&str]) -> Result<std::process::Output> {
let mut cmd = Command::new(git);
cmd.env_clear();
cmd.envs(std::env::vars_os());
cmd.current_dir(dir)
.args(args)
.output()
.with_context(|| format!("failed to execute {} in {}", git.display(), dir.display()))
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn is_false(b: &bool) -> bool {
!*b
}
pub(crate) fn trimmed_stderr(output: &std::process::Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();
if trimmed.is_empty() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
trimmed.to_string()
}
}
#[cfg(test)]
pub(crate) fn test_serial_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn is_false_is_the_skip_predicate_for_a_defaulted_bool() {
assert!(is_false(&false), "an unset flag is dropped from the wire");
assert!(!is_false(&true), "a set flag is serialized");
}
#[test]
fn trimmed_stderr_falls_back_to_stdout_when_stderr_is_empty() {
let with_stderr = std::process::Output {
status: std::process::Command::new("true").status().unwrap(),
stdout: b"out\n".to_vec(),
stderr: b" boom \n".to_vec(),
};
assert_eq!(trimmed_stderr(&with_stderr), "boom");
let stdout_only = std::process::Output {
status: std::process::Command::new("true").status().unwrap(),
stdout: b" fallback \n".to_vec(),
stderr: b" \n".to_vec(),
};
assert_eq!(
trimmed_stderr(&stdout_only),
"fallback",
"a whitespace-only stderr must not shadow a real stdout message"
);
}
}