pub(crate) fn run_git_diff(
base: &str,
head: &str,
cwd: Option<&std::path::Path>,
) -> anyhow::Result<String> {
let range = format!("{base}...{head}");
let mut command = std::process::Command::new("git");
command.args(["diff", &range]);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let output = command.output()?;
if !output.status.success() {
anyhow::bail!(
"git diff {range} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(String::from_utf8(output.stdout)?)
}
pub(crate) fn list_git_files(cwd: Option<&std::path::Path>) -> anyhow::Result<Vec<String>> {
let mut command = std::process::Command::new("git");
command.args(["ls-files"]);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let output = command.output()?;
if !output.status.success() {
anyhow::bail!(
"git ls-files failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(String::from_utf8(output.stdout)?
.lines()
.map(str::to_string)
.collect())
}
pub(crate) fn list_repo_files_for_outline(
cwd: Option<&std::path::Path>,
) -> anyhow::Result<Vec<String>> {
use anyhow::Context;
list_git_files(cwd).context(
"run rinkaku inside a git repository, or pipe a diff (e.g. `gh pr diff 123 | rinkaku`) \
or pass --base <ref>",
)
}
pub(crate) fn resolve_repo_root(cwd: Option<&std::path::Path>) -> std::path::PathBuf {
let mut command = std::process::Command::new("git");
command.args(["rev-parse", "--show-toplevel"]);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let toplevel = command
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|stdout| std::path::PathBuf::from(stdout.trim()));
toplevel.unwrap_or_else(|| match cwd {
Some(cwd) => cwd.to_path_buf(),
None => std::env::current_dir().unwrap_or_default(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::init_repo_with_committed_file;
use pretty_assertions::assert_eq;
#[test]
fn should_resolve_repository_root_when_cwd_is_a_subdirectory() {
let dir = tempfile::TempDir::new().expect("create tempdir");
init_repo_with_committed_file(dir.path(), "fn foo() {}\n");
let subdir = dir.path().join("src");
let actual = resolve_repo_root(Some(&subdir));
let expected = dir.path().canonicalize().expect("canonicalize expected");
let actual = actual.canonicalize().expect("canonicalize actual");
assert_eq!(expected, actual);
}
#[test]
fn should_fall_back_to_cwd_when_directory_is_not_a_git_repository() {
let dir = tempfile::TempDir::new().expect("create tempdir");
let actual = resolve_repo_root(Some(dir.path()));
assert_eq!(dir.path(), actual);
}
mod list_repo_files_for_outline_tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn should_include_guidance_in_error_when_cwd_is_not_a_git_repository() {
let dir = tempfile::TempDir::new().expect("create tempdir");
let actual = list_repo_files_for_outline(Some(dir.path()));
let error = actual.expect_err("a non-git directory must fail");
let message = format!("{error:#}");
assert!(
message.contains("run rinkaku inside a git repository"),
"error message did not contain the expected guidance: {message}"
);
}
#[test]
fn should_return_tracked_paths_when_cwd_is_a_git_repository() {
let dir = tempfile::TempDir::new().expect("create tempdir");
init_repo_with_committed_file(dir.path(), "fn foo() {}\n");
let actual = list_repo_files_for_outline(Some(dir.path()))
.expect("a git repository must succeed");
assert_eq!(vec!["src/lib.rs".to_string()], actual);
}
}
}