use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StartPoint {
Fresh { git_ref: String },
Stale { git_ref: String, reason: String },
LocalOnly,
UnverifiedHead { reason: String },
}
impl StartPoint {
pub fn git_ref(&self) -> Option<&str> {
match self {
StartPoint::Fresh { git_ref } | StartPoint::Stale { git_ref, .. } => Some(git_ref),
StartPoint::LocalOnly | StartPoint::UnverifiedHead { .. } => None,
}
}
pub fn warning(&self) -> Option<&str> {
match self {
StartPoint::Stale { reason, .. } | StartPoint::UnverifiedHead { reason } => {
Some(reason)
}
StartPoint::Fresh { .. } | StartPoint::LocalOnly => None,
}
}
}
pub fn resolve(base_path: &Path) -> StartPoint {
let Some(branch) = default_remote_branch(base_path) else {
return if has_origin_remote(base_path) {
StartPoint::UnverifiedHead {
reason: "could not resolve origin's default branch (origin/HEAD unset and no \
remote-tracking ref for the checked-out branch); branching from the \
base checkout's local HEAD, which may be behind the remote"
.to_string(),
}
} else {
StartPoint::LocalOnly
};
};
let tracking_ref = format!("refs/remotes/origin/{branch}");
let git_ref = format!("origin/{branch}");
let refspec = format!("+refs/heads/{branch}:{tracking_ref}");
match fetch(base_path, &refspec) {
Ok(()) => StartPoint::Fresh { git_ref },
Err(err) if ref_exists(base_path, &tracking_ref) => StartPoint::Stale {
reason: format!(
"git fetch origin {branch} failed ({err}); branching from the last fetched \
{git_ref} ({}) instead — this session may be behind the remote",
tip_description(base_path, &git_ref)
),
git_ref,
},
Err(err) => StartPoint::UnverifiedHead {
reason: format!(
"git fetch origin {branch} failed ({err}) and there is no {git_ref} \
remote-tracking ref to fall back on; branching from the base checkout's local \
HEAD, which may be behind the remote"
),
},
}
}
fn default_remote_branch(base_path: &Path) -> Option<String> {
if let Some(branch) = super::inproject_hygiene::get_default_branch(base_path) {
return Some(branch);
}
let current = git_stdout(base_path, &["symbolic-ref", "--short", "HEAD"])?;
ref_exists(base_path, &format!("refs/remotes/origin/{current}")).then_some(current)
}
fn fetch(base_path: &Path, refspec: &str) -> Result<(), String> {
let out = git(base_path)
.args(["fetch", "--no-tags", "origin", refspec])
.output()
.map_err(|e| format!("failed to spawn git fetch: {e}"))?;
if out.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&out.stderr);
Err(format!(
"exit {}: {}",
out.status,
stderr.trim().replace('\n', "; ")
))
}
fn has_origin_remote(base_path: &Path) -> bool {
git_stdout(base_path, &["remote", "get-url", "origin"]).is_some()
}
fn ref_exists(base_path: &Path, full_ref: &str) -> bool {
git(base_path)
.args(["rev-parse", "--verify", "--quiet", full_ref])
.output()
.map(|out| out.status.success())
.unwrap_or(false)
}
fn tip_description(base_path: &Path, git_ref: &str) -> String {
git_stdout(base_path, &["log", "-1", "--format=%h %cs", git_ref])
.unwrap_or_else(|| "unknown tip".to_string())
}
fn git_stdout(base_path: &Path, args: &[&str]) -> Option<String> {
let out = git(base_path).args(args).output().ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!s.is_empty()).then_some(s)
}
fn git(base_path: &Path) -> Command {
let mut cmd = trusty_common::git::command();
cmd.env("GIT_TERMINAL_PROMPT", "0").arg("-C").arg(base_path);
cmd
}