Skip to main content

rskit_git/embedded/
repository.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use rskit_errors::{AppError, AppResult};
5
6use crate::auth::{AuthProvider, DefaultAuthProvider};
7use crate::core::Repository as RepositoryTrait;
8use crate::error::GitError;
9use crate::options::InitOptions;
10use crate::types::{DEFAULT_BRANCH, Oid, Reference};
11
12use super::{map_head_error, oid_from_git2, reference_from_git2};
13
14/// Repository implementation backed by libgit2.
15pub struct Git2Repository {
16    pub(crate) repo: git2::Repository,
17    pub(crate) root: PathBuf,
18    pub(crate) auth: Arc<dyn AuthProvider>,
19}
20
21impl Git2Repository {
22    /// Returns the repository root path.
23    pub fn root(&self) -> &Path {
24        &self.root
25    }
26}
27
28fn default_auth() -> Arc<dyn AuthProvider> {
29    Arc::new(DefaultAuthProvider)
30}
31
32/// Opens a git repository at the given path (canonicalized).
33pub fn open(path: impl AsRef<Path>) -> AppResult<Git2Repository> {
34    open_with_auth(path, default_auth())
35}
36
37/// Opens a git repository at the given path with an explicit auth provider.
38pub fn open_with_auth(
39    path: impl AsRef<Path>,
40    auth: Arc<dyn AuthProvider>,
41) -> AppResult<Git2Repository> {
42    let path = path.as_ref();
43    let abs = std::fs::canonicalize(path).map_err(|err| match err.kind() {
44        std::io::ErrorKind::NotFound => GitError::NotFound {
45            path: path.to_path_buf(),
46        },
47        _ => GitError::Internal(git2::Error::from_str(&err.to_string())),
48    })?;
49    let repo = git2::Repository::open(&abs).map_err(|err| {
50        if err.code() == git2::ErrorCode::NotFound {
51            GitError::NotFound { path: abs.clone() }
52        } else {
53            GitError::Internal(err)
54        }
55    })?;
56    // workdir() is None for bare repos; fall back to the .git dir path
57    let root = repo.workdir().unwrap_or_else(|| repo.path()).to_path_buf();
58    Ok(Git2Repository { repo, root, auth })
59}
60
61/// Discovers a git repository by walking up from the given path.
62pub fn discover(path: impl AsRef<Path>) -> AppResult<Git2Repository> {
63    discover_with_auth(path, default_auth())
64}
65
66/// Discovers a git repository by walking up from `path` with an explicit auth provider.
67pub fn discover_with_auth(
68    path: impl AsRef<Path>,
69    auth: Arc<dyn AuthProvider>,
70) -> AppResult<Git2Repository> {
71    let path = path.as_ref();
72    let repo = git2::Repository::discover(path).map_err(|err| {
73        if err.code() == git2::ErrorCode::NotFound {
74            GitError::NotFound {
75                path: path.to_path_buf(),
76            }
77        } else {
78            GitError::Internal(err)
79        }
80    })?;
81    // workdir() is None for bare repos; fall back to the .git dir path
82    let root = repo.workdir().unwrap_or_else(|| repo.path()).to_path_buf();
83    Ok(Git2Repository { repo, root, auth })
84}
85
86/// Clones a git repository into the given path.
87pub fn clone(url: &str, path: impl AsRef<Path>) -> AppResult<Git2Repository> {
88    let path = path.as_ref();
89    let repo = git2::Repository::clone(url, path).map_err(GitError::Internal)?;
90    let root = repo
91        .workdir()
92        .map(Path::to_path_buf)
93        .unwrap_or_else(|| path.to_path_buf());
94    Ok(Git2Repository {
95        repo,
96        root,
97        auth: default_auth(),
98    })
99}
100
101/// Creates a new git repository at the given path.
102///
103/// The initial branch is [`DEFAULT_BRANCH`], pinned explicitly so the trunk
104/// name does not depend on the host's Git configuration.
105pub fn init(path: impl AsRef<Path>) -> AppResult<Git2Repository> {
106    init_with(
107        path,
108        &InitOptions::default().with_initial_branch(DEFAULT_BRANCH),
109    )
110}
111
112/// Creates a new git repository at the given path with explicit options.
113pub fn init_with(path: impl AsRef<Path>, options: &InitOptions) -> AppResult<Git2Repository> {
114    let path = path.as_ref();
115    let mut git2_options = git2::RepositoryInitOptions::new();
116    if let Some(initial_branch) = &options.initial_branch {
117        let reference = format!("refs/heads/{initial_branch}");
118        if !git2::Reference::is_valid_name(&reference) {
119            return Err(AppError::invalid_input(
120                "initial_branch",
121                format!("invalid branch name '{initial_branch}'"),
122            ));
123        }
124        git2_options.initial_head(initial_branch);
125    }
126    let repo = git2::Repository::init_opts(path, &git2_options).map_err(GitError::Internal)?;
127    let root = repo
128        .workdir()
129        .map(Path::to_path_buf)
130        .unwrap_or_else(|| path.to_path_buf());
131    Ok(Git2Repository {
132        repo,
133        root,
134        auth: default_auth(),
135    })
136}
137
138/// Creates a new bare git repository at the given path.
139///
140/// The initial branch is [`DEFAULT_BRANCH`], matching [`init`].
141pub fn init_bare(path: impl AsRef<Path>) -> AppResult<Git2Repository> {
142    let path = path.as_ref();
143    let mut options = git2::RepositoryInitOptions::new();
144    options.bare(true).initial_head(DEFAULT_BRANCH);
145    let repo = git2::Repository::init_opts(path, &options).map_err(GitError::Internal)?;
146    Ok(Git2Repository {
147        repo,
148        root: path.to_path_buf(),
149        auth: default_auth(),
150    })
151}
152
153impl RepositoryTrait for Git2Repository {
154    fn root(&self) -> &Path {
155        &self.root
156    }
157
158    fn head(&self) -> AppResult<Reference> {
159        let head = self.repo.head().map_err(map_head_error)?;
160        Ok(reference_from_git2(&head))
161    }
162
163    fn resolve_ref(&self, refname: &str) -> AppResult<Oid> {
164        let obj = self
165            .repo
166            .revparse_single(refname)
167            .map_err(|_| GitError::RefNotFound {
168                refname: refname.to_string(),
169            })?;
170        Ok(oid_from_git2(obj.id()))
171    }
172
173    fn is_dirty(&self) -> AppResult<bool> {
174        let statuses = self.repo.statuses(None).map_err(GitError::Internal)?;
175        Ok(!statuses.is_empty())
176    }
177}