pub(crate) fn fetch_pr_head(number: u64, cwd: Option<&std::path::Path>) -> anyhow::Result<String> {
run_git_fetch(&format!("refs/pull/{number}/head"), cwd)
}
pub(crate) fn fetch_branch_head(
name: &str,
cwd: Option<&std::path::Path>,
) -> anyhow::Result<String> {
run_git_fetch(name, cwd)
}
pub(crate) fn resolve_pr_base_sha(
base_ref_oid: &str,
mut object_exists: impl FnMut(&str) -> bool,
mut fetch_base_branch: impl FnMut() -> anyhow::Result<String>,
mut fetch_oid: impl FnMut(&str) -> anyhow::Result<()>,
) -> anyhow::Result<(String, bool)> {
if object_exists(base_ref_oid) {
return Ok((base_ref_oid.to_string(), false));
}
let branch_tip = match fetch_base_branch() {
Ok(tip) => {
if object_exists(base_ref_oid) {
return Ok((base_ref_oid.to_string(), false));
}
Some(tip)
}
Err(source) => {
log::warn!(
"fetching the base branch failed, continuing the base-commit resolution \
cascade: {source}"
);
None
}
};
if fetch_oid(base_ref_oid).is_ok() && object_exists(base_ref_oid) {
return Ok((base_ref_oid.to_string(), false));
}
let branch_tip = match branch_tip {
Some(tip) => tip,
None => fetch_base_branch()?,
};
Ok((branch_tip, true))
}
pub(crate) fn object_exists_locally(cwd: Option<&std::path::Path>, oid: &str) -> bool {
let mut command = std::process::Command::new("git");
command.args(["cat-file", "-e", &format!("{oid}^{{commit}}")]);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
command.output().is_ok_and(|output| output.status.success())
}
pub(crate) fn fetch_oid(cwd: Option<&std::path::Path>, oid: &str) -> anyhow::Result<()> {
let mut command = std::process::Command::new("git");
command.args(["fetch", "origin", oid]);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let output = command.output()?;
if !output.status.success() {
anyhow::bail!(
"git fetch origin {oid} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
fn run_git_fetch(refspec: &str, cwd: Option<&std::path::Path>) -> anyhow::Result<String> {
let mut fetch_command = std::process::Command::new("git");
fetch_command.args(["fetch", "origin", refspec]);
if let Some(cwd) = cwd {
fetch_command.current_dir(cwd);
}
let fetch_output = fetch_command.output()?;
if !fetch_output.status.success() {
anyhow::bail!(
"git fetch origin {refspec} failed: {}",
String::from_utf8_lossy(&fetch_output.stderr)
);
}
let mut rev_parse_command = std::process::Command::new("git");
rev_parse_command.args(["rev-parse", "FETCH_HEAD"]);
if let Some(cwd) = cwd {
rev_parse_command.current_dir(cwd);
}
let rev_parse_output = rev_parse_command.output()?;
if !rev_parse_output.status.success() {
anyhow::bail!(
"git rev-parse FETCH_HEAD failed after fetching {refspec}: {}",
String::from_utf8_lossy(&rev_parse_output.stderr)
);
}
Ok(String::from_utf8(rev_parse_output.stdout)?
.trim()
.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
mod resolve_pr_base_sha_tests {
use super::*;
use pretty_assertions::assert_eq;
use std::cell::RefCell;
#[test]
fn should_return_base_ref_oid_when_it_already_exists_locally() {
let fetch_base_branch_calls = RefCell::new(0);
let fetch_oid_calls = RefCell::new(0);
let actual = resolve_pr_base_sha(
"base789",
|_oid| true,
|| {
*fetch_base_branch_calls.borrow_mut() += 1;
Ok("branch-tip-sha".to_string())
},
|_oid| {
*fetch_oid_calls.borrow_mut() += 1;
Ok(())
},
)
.expect("should resolve without error");
assert_eq!(("base789".to_string(), false), actual);
assert_eq!(0, *fetch_base_branch_calls.borrow());
assert_eq!(0, *fetch_oid_calls.borrow());
}
#[test]
fn should_return_base_ref_oid_when_fetching_the_base_branch_makes_it_available() {
let exists_calls = RefCell::new(0);
let object_exists = |_oid: &str| {
let mut calls = exists_calls.borrow_mut();
*calls += 1;
*calls > 1
};
let actual = resolve_pr_base_sha(
"base789",
object_exists,
|| Ok("branch-tip-sha".to_string()),
|_oid| panic!("fetch_oid must not be called when the base branch fetch sufficed"),
)
.expect("should resolve without error");
assert_eq!(("base789".to_string(), false), actual);
}
#[test]
fn should_return_base_ref_oid_when_fetching_the_oid_directly_makes_it_available() {
let exists_calls = RefCell::new(0);
let object_exists = |_oid: &str| {
let mut calls = exists_calls.borrow_mut();
*calls += 1;
*calls > 2
};
let actual = resolve_pr_base_sha(
"base789",
object_exists,
|| Ok("branch-tip-sha".to_string()),
|_oid| Ok(()),
)
.expect("should resolve without error");
assert_eq!(("base789".to_string(), false), actual);
}
#[test]
fn should_fall_back_to_branch_tip_when_the_oid_is_unreachable_by_any_means() {
let actual = resolve_pr_base_sha(
"base789",
|_oid| false,
|| Ok("branch-tip-sha".to_string()),
|_oid| anyhow::bail!("simulated: base789 not found on the remote"),
)
.expect("should fall back rather than error");
assert_eq!(("branch-tip-sha".to_string(), true), actual);
}
#[test]
fn should_fall_back_to_branch_tip_when_fetch_oid_succeeds_but_object_still_missing() {
let actual = resolve_pr_base_sha(
"base789",
|_oid| false,
|| Ok("branch-tip-sha".to_string()),
|_oid| Ok(()),
)
.expect("should fall back rather than error");
assert_eq!(("branch-tip-sha".to_string(), true), actual);
}
#[test]
fn should_fall_through_to_fetch_oid_when_fetching_the_base_branch_fails() {
let exists_calls = RefCell::new(0);
let object_exists = |_oid: &str| {
let mut calls = exists_calls.borrow_mut();
*calls += 1;
*calls > 1
};
let actual = resolve_pr_base_sha(
"base789",
object_exists,
|| anyhow::bail!("simulated: base branch was deleted"),
|_oid| Ok(()),
)
.expect("a step-2 failure must not abort the cascade");
assert_eq!(("base789".to_string(), false), actual);
}
#[test]
fn should_fetch_branch_tip_for_fallback_when_step_two_failed_and_fetch_oid_also_fails() {
let fetch_base_branch_calls = RefCell::new(0);
let actual = resolve_pr_base_sha(
"base789",
|_oid| false,
|| {
let mut calls = fetch_base_branch_calls.borrow_mut();
*calls += 1;
if *calls == 1 {
anyhow::bail!("simulated: base branch was deleted")
} else {
Ok("branch-tip-sha".to_string())
}
},
|_oid| anyhow::bail!("simulated: base789 not found on the remote"),
)
.expect("should fall back rather than error");
assert_eq!(("branch-tip-sha".to_string(), true), actual);
assert_eq!(2, *fetch_base_branch_calls.borrow());
}
#[test]
fn should_reuse_step_two_tip_for_fallback_without_refetching() {
let fetch_base_branch_calls = RefCell::new(0);
let actual = resolve_pr_base_sha(
"base789",
|_oid| false,
|| {
*fetch_base_branch_calls.borrow_mut() += 1;
Ok("branch-tip-sha".to_string())
},
|_oid| anyhow::bail!("simulated: base789 not found on the remote"),
)
.expect("should fall back rather than error");
assert_eq!(("branch-tip-sha".to_string(), true), actual);
assert_eq!(
1,
*fetch_base_branch_calls.borrow(),
"fetch_base_branch must only be called once (by step 2); step 4 must reuse its \
result instead of fetching the base branch again"
);
}
#[test]
fn should_propagate_error_when_the_branch_tip_fallback_itself_fails() {
let fetch_base_branch_calls = RefCell::new(0);
let actual = resolve_pr_base_sha(
"base789",
|_oid| false,
|| {
let mut calls = fetch_base_branch_calls.borrow_mut();
*calls += 1;
anyhow::bail!("simulated: git fetch origin main failed")
},
|_oid| anyhow::bail!("simulated: base789 not found on the remote"),
);
assert!(actual.is_err());
}
}
}