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