git_lfs_git/lib.rs
1//! Git interop helpers for Git LFS: config, refs, scanners, and `.gitattributes` matching.
2//!
3//! Git LFS needs the user's git binary for a handful of things
4//! with no LFS-specific equivalent: where the repo lives, what's
5//! in its config, which objects each ref reaches, and how
6//! `.gitattributes` applies to a given path. This crate collects
7//! those helpers in one place. Everything runs by shelling out
8//! to the `git` binary the user has installed; this crate does
9//! not bundle its own git implementation.
10//!
11//! It sits at the bottom of the LFS workspace: every other crate
12//! goes through it whenever it needs to know something about the
13//! repo it's running against. The crate is intentionally a
14//! collection of unrelated helpers rather than a single
15//! abstraction, so the pieces are independent of each other and
16//! you can pick what you need. See the per-module docs below for
17//! the specific surfaces.
18//!
19//! [`Error`] is the shared error type for the few cases that
20//! need to surface git's stderr verbatim.
21
22use std::io;
23use std::path::Path;
24use std::process::Command;
25
26pub mod aliases;
27pub mod attr;
28pub mod cat_file;
29pub mod config;
30pub mod diff_index;
31pub mod endpoint;
32pub mod extension;
33pub mod fetch_prune;
34pub mod http_options;
35pub mod path;
36pub mod pktline;
37pub mod refs;
38pub mod rev_list;
39pub mod scanner;
40
41// Top-level re-exports: a small set of widely-used helpers that
42// feel natural as flat names. Everything else is accessed through
43// its module — see the module list rendered below by rustdoc.
44pub use attr::AttrSet;
45pub use config::ConfigScope;
46pub use http_options::{HttpOptions, extra_headers_for, lfs_url_bool};
47pub use path::{git_common_dir, git_dir, lfs_alternate_dirs, lfs_dir, work_tree_root};
48
49#[derive(Debug, thiserror::Error)]
50pub enum Error {
51 #[error("io error invoking git: {0}")]
52 Io(#[from] io::Error),
53 #[error("git: {0}")]
54 Failed(String),
55}
56
57/// Run `git -C <cwd> <args>` and return its trimmed stdout on success.
58pub(crate) fn run_git(cwd: &Path, args: &[&str]) -> Result<String, Error> {
59 let out = Command::new("git").arg("-C").arg(cwd).args(args).output()?;
60 if !out.status.success() {
61 return Err(Error::Failed(
62 String::from_utf8_lossy(&out.stderr).trim().to_owned(),
63 ));
64 }
65 Ok(String::from_utf8(out.stdout)
66 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
67 .trim()
68 .to_owned())
69}
70
71#[cfg(test)]
72pub(crate) mod tests {
73 /// Shared test helpers for setting up real git repos. Each helper
74 /// function shells out to the `git` binary; tests using these are
75 /// integration-level despite living next to their module.
76 pub mod commit_helper {
77 use std::path::Path;
78 use std::process::Command;
79
80 use tempfile::TempDir;
81
82 /// Initialize a fresh repo with a deterministic identity + branch
83 /// so tests don't depend on the developer's git config.
84 pub fn init_repo() -> TempDir {
85 // Fail loudly if the test process inherits GIT_DIR /
86 // GIT_WORK_TREE. With those set, `git init <tempdir>`
87 // ignores the path and operates on the inherited git-dir
88 // instead — every subsequent assertion would silently
89 // exercise the wrong repo. The `Justfile` pre-commit
90 // recipe strips these; this is the canary if anything
91 // else slips through.
92 for var in ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"] {
93 assert!(
94 std::env::var_os(var).is_none(),
95 "{var} is set in the test process — git subprocesses \
96 will ignore the per-test tempdir. Run via \
97 `just pre-commit` (which strips it) or \
98 `env -u {var} cargo test`."
99 );
100 }
101 let tmp = TempDir::new().unwrap();
102 run(tmp.path(), &["init", "--quiet", "--initial-branch=main"]);
103 run(tmp.path(), &["config", "user.email", "test@example.com"]);
104 run(tmp.path(), &["config", "user.name", "test"]);
105 // Disable signing so contributor environments with sign.commit
106 // configured globally don't fail tests.
107 run(tmp.path(), &["config", "commit.gpgsign", "false"]);
108 tmp
109 }
110
111 /// Add and commit `content` at `path` (relative to the repo root).
112 /// Returns nothing — call `head_oid` if you need the resulting
113 /// commit's SHA.
114 pub fn commit_file(repo: &TempDir, path: &str, content: &[u8]) {
115 std::fs::write(repo.path().join(path), content).unwrap();
116 run(repo.path(), &["add", path]);
117 run(
118 repo.path(),
119 &["commit", "--quiet", "-m", &format!("add {path}")],
120 );
121 }
122
123 /// Hex OID of the commit currently at HEAD.
124 pub fn head_oid(repo: &TempDir) -> String {
125 let out = Command::new("git")
126 .arg("-C")
127 .arg(repo.path())
128 .args(["rev-parse", "HEAD"])
129 .output()
130 .unwrap();
131 assert!(out.status.success());
132 String::from_utf8_lossy(&out.stdout).trim().to_owned()
133 }
134
135 fn run(cwd: &Path, args: &[&str]) {
136 let status = Command::new("git")
137 .arg("-C")
138 .arg(cwd)
139 .args(args)
140 .status()
141 .unwrap();
142 assert!(status.success(), "git {args:?} failed");
143 }
144 }
145}