pub(crate) mod commit_files;
pub(crate) mod github;
pub(crate) mod graph;
pub(crate) mod graph_render;
pub(crate) mod scanner;
pub(crate) mod status;
use std::io;
use std::sync::OnceLock;
pub(crate) fn describe_spawn_error(e: &io::Error) -> String {
if e.kind() == io::ErrorKind::NotFound {
"git is not installed or not on PATH".to_string()
} else {
e.to_string()
}
}
pub(crate) fn git_available() -> bool {
static AVAILABLE: OnceLock<bool> = OnceLock::new();
*AVAILABLE.get_or_init(|| {
std::process::Command::new("git")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
})
}
pub(crate) fn preferred_remote(repo: &git2::Repository) -> Option<String> {
let mut remotes: Vec<String> = repo
.remotes()
.ok()?
.iter()
.filter_map(|r| r.ok().flatten().map(|s| s.to_string()))
.collect();
if remotes.iter().any(|r| r == "origin") {
return Some("origin".into());
}
remotes.sort();
remotes.into_iter().next()
}
pub(crate) fn resolve_sync_remote(
path: &std::path::Path,
branch: &str,
push: bool,
) -> Option<String> {
let repo = git2::Repository::open(path).ok()?;
let config = repo.config().ok()?;
let get = |key: &str| config.get_string(key).ok();
let upstream = get(&format!("branch.{branch}.remote"));
let has_merge = get(&format!("branch.{branch}.merge")).is_some();
let has_push_dest = push
&& (get(&format!("branch.{branch}.pushremote")).is_some()
|| get("remote.pushdefault").is_some());
if (upstream.is_some() && has_merge) || has_push_dest {
return None;
}
if let Some(remote) = upstream
&& repo.find_remote(&remote).is_ok()
{
return Some(remote);
}
preferred_remote(&repo)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn describe_spawn_error_flags_missing_git() {
let err = io::Error::new(io::ErrorKind::NotFound, "No such file or directory");
assert_eq!(
describe_spawn_error(&err),
"git is not installed or not on PATH"
);
}
#[test]
fn describe_spawn_error_passes_through_other_errors() {
let err = io::Error::new(io::ErrorKind::PermissionDenied, "permission denied");
assert_eq!(describe_spawn_error(&err), "permission denied");
}
fn repo_with_remotes(remotes: &[&str]) -> (tempfile::TempDir, git2::Repository) {
let tmp = tempfile::TempDir::new().unwrap();
let repo = git2::Repository::init(tmp.path()).unwrap();
for (i, name) in remotes.iter().enumerate() {
repo.remote(name, &format!("https://example.com/repo{i}.git"))
.unwrap();
}
(tmp, repo)
}
#[test]
fn sync_remote_prefers_origin_without_upstream() {
let (tmp, _repo) = repo_with_remotes(&["origin", "gerrit"]);
assert_eq!(
resolve_sync_remote(tmp.path(), "main", false).as_deref(),
Some("origin")
);
}
#[test]
fn sync_remote_uses_sole_remote_when_no_origin() {
let (tmp, _repo) = repo_with_remotes(&["gerrit"]);
assert_eq!(
resolve_sync_remote(tmp.path(), "main", false).as_deref(),
Some("gerrit")
);
}
#[test]
fn sync_remote_picks_first_lexicographic_without_origin() {
let (tmp, _repo) = repo_with_remotes(&["zzz", "gerrit"]);
assert_eq!(
resolve_sync_remote(tmp.path(), "main", false).as_deref(),
Some("gerrit")
);
}
#[test]
fn sync_remote_none_without_remotes() {
let (tmp, _repo) = repo_with_remotes(&[]);
assert_eq!(resolve_sync_remote(tmp.path(), "main", false), None);
}
#[test]
fn sync_remote_defers_to_configured_upstream() {
let (tmp, repo) = repo_with_remotes(&["origin", "gerrit"]);
let mut config = repo.config().unwrap();
config.set_str("branch.main.remote", "gerrit").unwrap();
config
.set_str("branch.main.merge", "refs/heads/master")
.unwrap();
assert_eq!(resolve_sync_remote(tmp.path(), "main", false), None);
assert_eq!(resolve_sync_remote(tmp.path(), "main", true), None);
}
#[test]
fn sync_remote_prefers_branch_remote_over_origin() {
let (tmp, repo) = repo_with_remotes(&["origin", "gerrit"]);
let mut config = repo.config().unwrap();
config.set_str("branch.main.remote", "gerrit").unwrap();
assert_eq!(
resolve_sync_remote(tmp.path(), "main", false).as_deref(),
Some("gerrit")
);
}
#[test]
fn sync_remote_defers_to_push_remote_only_for_push() {
let (tmp, repo) = repo_with_remotes(&["origin", "gerrit"]);
let mut config = repo.config().unwrap();
config.set_str("branch.main.pushremote", "gerrit").unwrap();
assert_eq!(resolve_sync_remote(tmp.path(), "main", true), None);
assert_eq!(
resolve_sync_remote(tmp.path(), "main", false).as_deref(),
Some("origin")
);
}
}