Skip to main content

omni_dev/
git.rs

1//! Git operations and repository management.
2
3use std::path::{Path, PathBuf};
4
5pub mod amendment;
6pub mod commit;
7pub mod diff_split;
8pub mod lint;
9pub mod main_branches;
10pub mod remote;
11pub mod repository;
12pub mod worktree_batch;
13pub mod worktree_push;
14pub mod worktree_rebase;
15
16pub use amendment::AmendmentHandler;
17pub use commit::{
18    refine_message_scope, resolve_scope, CommitAnalysis, CommitAnalysisForAI, CommitInfo,
19    CommitInfoForAI, FileDiffRef,
20};
21pub use diff_split::{split_by_file, split_file_by_hunk, FileDiff, HunkDiff};
22pub use lint::{
23    lint_message, parse_subject, passes as lint_passes, suggest_scope_fix, ParsedSubject,
24};
25pub use main_branches::{branches_containing, detect_main_branch_tips, MainBranchTip};
26pub use remote::RemoteInfo;
27pub use repository::GitRepository;
28
29/// Number of hex characters to show in abbreviated commit hashes.
30pub const SHORT_HASH_LEN: usize = 8;
31
32/// Length of a full SHA-1 commit hash in hex characters.
33pub const FULL_HASH_LEN: usize = 40;
34
35/// Environment override for the `git` binary, for when a process runs under
36/// launchd/systemd with a minimal `PATH`. The exact analogue of
37/// `OMNI_DEV_GH_BIN` (`crate::pr_status`) and `OMNI_DEV_VSCODE_BIN` (the tray's
38/// `code` launcher).
39const GIT_BIN_ENV: &str = "OMNI_DEV_GIT_BIN";
40
41/// Absolute paths probed for `git` when [`GIT_BIN_ENV`] is unset, in order.
42///
43/// The daemon cannot rely on `PATH`: launchd hands it
44/// `/usr/bin:/bin:/usr/sbin:/sbin`. On macOS that *does* contain `/usr/bin/git`
45/// (the Xcode command-line-tools shim), so the fallback would work โ€” but it
46/// would silently pick a different `git` than the user's shell does, which for
47/// a history-rewriting operation is exactly the kind of divergence worth ruling
48/// out. Homebrew first, therefore, matching [`GH_BINARY_CANDIDATES`] order.
49///
50/// [`GH_BINARY_CANDIDATES`]: crate::pr_status
51const GIT_BINARY_CANDIDATES: &[&str] = &[
52    "/opt/homebrew/bin/git",
53    "/usr/local/bin/git",
54    "/home/linuxbrew/.linuxbrew/bin/git",
55    "/usr/bin/git",
56];
57
58/// Resolves `git`, preferring [`GIT_BIN_ENV`], then the first existing
59/// well-known absolute path, then bare `git` on `PATH`.
60///
61/// This is the fix for the *real* obstacle to running git from the daemon
62/// (ADR-0059). The obstacle was never credentials โ€” launchd exports
63/// `SSH_AUTH_SOCK` into the per-user session, so a LaunchAgent inherits the
64/// user's `ssh-agent` โ€” it was the minimal `PATH`, the same problem
65/// [ADR-0049](../docs/adrs/adr-0049.md) ยง3 solves for the `code` launcher and
66/// [`resolve_gh_binary`](crate::pr_status::resolve_gh_binary) solves for `gh`.
67///
68/// Callers should do this **once** and pass the result down, rather than
69/// re-reading the environment per subprocess.
70#[must_use]
71pub fn resolve_git_binary() -> PathBuf {
72    resolve_git_binary_from(std::env::var_os(GIT_BIN_ENV), GIT_BINARY_CANDIDATES)
73}
74
75/// The testable core of [`resolve_git_binary`]. Split so the probe order can be
76/// unit-tested without mutating the process environment (#1030).
77fn resolve_git_binary_from(
78    env_override: Option<std::ffi::OsString>,
79    candidates: &[&str],
80) -> PathBuf {
81    if let Some(path) = env_override.filter(|p| !p.is_empty()) {
82        return PathBuf::from(path);
83    }
84    for candidate in candidates {
85        let path = Path::new(candidate);
86        if path.exists() {
87            return path.to_path_buf();
88        }
89    }
90    PathBuf::from("git")
91}
92
93#[cfg(test)]
94#[allow(clippy::unwrap_used)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn resolve_git_binary_from_prefers_env_then_candidate_then_fallback() {
100        assert_eq!(
101            resolve_git_binary_from(Some("/custom/git".into()), &["/usr/bin/git"]),
102            PathBuf::from("/custom/git"),
103            "an explicit override wins over every candidate"
104        );
105        // A path guaranteed to exist on every platform the suite runs on.
106        let existing = std::env::current_exe().unwrap();
107        let existing = existing.to_str().unwrap();
108        assert_eq!(
109            resolve_git_binary_from(None, &["/no/such/git/xyzzy", existing]),
110            PathBuf::from(existing),
111            "the first *existing* candidate wins, not merely the first"
112        );
113        assert_eq!(
114            resolve_git_binary_from(None, &["/no/such/git/xyzzy"]),
115            PathBuf::from("git"),
116            "with nothing found, fall back to a bare PATH lookup"
117        );
118        assert_eq!(
119            resolve_git_binary_from(Some(String::new().into()), &["/no/such/git/xyzzy"]),
120            PathBuf::from("git"),
121            "an empty override is ignored rather than spawning \"\""
122        );
123    }
124
125    #[test]
126    fn resolve_git_binary_reads_the_real_environment() {
127        // Smoke: the public wrapper must not panic on whatever this machine has.
128        let _ = resolve_git_binary();
129    }
130}