Skip to main content

git_lfs_git/
lib.rs

1//! Git interop for git-lfs.
2//!
3//! Everything in this crate shells out to the `git` binary — see CLAUDE.md
4//! for the rationale.
5
6use std::io;
7use std::path::Path;
8use std::process::Command;
9
10pub mod attr;
11pub mod cat_file;
12pub mod config;
13pub mod diff_index;
14pub mod endpoint;
15pub mod path;
16pub mod pktline;
17pub mod refs;
18pub mod rev_list;
19pub mod scanner;
20
21pub use attr::AttrSet;
22pub use cat_file::{BlobContent, CatFileBatch, CatFileBatchCheck, CatFileHeader};
23pub use config::ConfigScope;
24pub use diff_index::{DiffEntry, diff_index};
25pub use endpoint::{EndpointError, derive_lfs_url, endpoint_for_remote};
26pub use path::{git_dir, lfs_dir};
27pub use rev_list::{RevListEntry, rev_list};
28pub use scanner::{PointerEntry, TreeBlob, scan_pointers, scan_tree, scan_tree_blobs};
29
30#[derive(Debug, thiserror::Error)]
31pub enum Error {
32    #[error("io error invoking git: {0}")]
33    Io(#[from] io::Error),
34    #[error("git: {0}")]
35    Failed(String),
36}
37
38/// Run `git -C <cwd> <args>` and return its trimmed stdout on success.
39pub(crate) fn run_git(cwd: &Path, args: &[&str]) -> Result<String, Error> {
40    let out = Command::new("git").arg("-C").arg(cwd).args(args).output()?;
41    if !out.status.success() {
42        return Err(Error::Failed(
43            String::from_utf8_lossy(&out.stderr).trim().to_owned(),
44        ));
45    }
46    Ok(String::from_utf8(out.stdout)
47        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
48        .trim()
49        .to_owned())
50}
51
52#[cfg(test)]
53pub(crate) mod tests {
54    /// Shared test helpers for setting up real git repos. Each helper
55    /// function shells out to the `git` binary; tests using these are
56    /// integration-level despite living next to their module.
57    pub mod commit_helper {
58        use std::path::Path;
59        use std::process::Command;
60
61        use tempfile::TempDir;
62
63        /// Initialize a fresh repo with a deterministic identity + branch
64        /// so tests don't depend on the developer's git config.
65        pub fn init_repo() -> TempDir {
66            let tmp = TempDir::new().unwrap();
67            run(tmp.path(), &["init", "--quiet", "--initial-branch=main"]);
68            run(tmp.path(), &["config", "user.email", "test@example.com"]);
69            run(tmp.path(), &["config", "user.name", "test"]);
70            // Disable signing so contributor environments with sign.commit
71            // configured globally don't fail tests.
72            run(tmp.path(), &["config", "commit.gpgsign", "false"]);
73            tmp
74        }
75
76        /// Add and commit `content` at `path` (relative to the repo root).
77        /// Returns nothing — call `head_oid` if you need the resulting
78        /// commit's SHA.
79        pub fn commit_file(repo: &TempDir, path: &str, content: &[u8]) {
80            std::fs::write(repo.path().join(path), content).unwrap();
81            run(repo.path(), &["add", path]);
82            run(repo.path(), &["commit", "--quiet", "-m", &format!("add {path}")]);
83        }
84
85        /// Hex OID of the commit currently at HEAD.
86        pub fn head_oid(repo: &TempDir) -> String {
87            let out = Command::new("git")
88                .arg("-C")
89                .arg(repo.path())
90                .args(["rev-parse", "HEAD"])
91                .output()
92                .unwrap();
93            assert!(out.status.success());
94            String::from_utf8_lossy(&out.stdout).trim().to_owned()
95        }
96
97        fn run(cwd: &Path, args: &[&str]) {
98            let status = Command::new("git").arg("-C").arg(cwd).args(args).status().unwrap();
99            assert!(status.success(), "git {args:?} failed");
100        }
101    }
102}