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 { git_ref: String },
UnverifiedHead { reason: String },
}
impl StartPoint {
pub fn git_ref(&self) -> Option<&str> {
match self {
StartPoint::Fresh { git_ref }
| StartPoint::Stale { git_ref, .. }
| StartPoint::LocalOnly { git_ref } => Some(git_ref),
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) -> Result<StartPoint, String> {
let Some(branch) = default_remote_branch(base_path) else {
let local_only =
crate::core::remote_mode::remote_mode(base_path).is_ok_and(|mode| mode.is_local_only());
if local_only {
let branch = crate::core::remote_mode::local_default_branch(base_path)?;
let git_ref = format!("refs/heads/{branch}");
return Ok(StartPoint::LocalOnly { git_ref });
}
return Ok(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(),
});
};
let tracking_ref = format!("refs/remotes/origin/{branch}");
let git_ref = format!("origin/{branch}");
let refspec = format!("+refs/heads/{branch}:{tracking_ref}");
Ok(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(());
}
Err(fetch_failure(&out.status, &out.stderr))
}
fn fetch_failure(status: &impl std::fmt::Display, stderr: &[u8]) -> String {
let stderr = String::from_utf8_lossy(stderr);
let stderr = crate::core::remote_url_redact::redact_url(stderr.trim()).replace('\n', "; ");
format!("exit {status}: {stderr}")
}
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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fetch_failure_never_quotes_the_remote_token() {
const TOKEN: &str = "ghp_9124FetchSyntheticToken000000";
let stderr = format!(
"fatal: unable to access 'https://octo:{TOKEN}@github.com/acme/widget.git/': \
Could not resolve host\nfatal: the remote end hung up"
);
let msg = fetch_failure(&"exit status: 128", stderr.as_bytes());
assert!(!msg.contains(TOKEN), "the token reached the fetch error");
assert!(
msg.contains("'https://***@github.com/acme/widget.git/'"),
"{msg}"
);
assert!(msg.contains("; fatal: the remote end hung up"), "{msg}");
}
}