simple_git/
lib.rs

1use std::{fmt, mem, num::NonZeroU32, path::Path, str::FromStr, sync::atomic::AtomicBool};
2
3use gix::{
4    clone, create,
5    open::{self, Permissions},
6    remote, Url,
7};
8use tracing::debug;
9
10mod progress_tracing;
11use progress_tracing::TracingProgress;
12
13mod cancellation_token;
14pub use cancellation_token::{GitCancelOnDrop, GitCancellationToken};
15
16mod error;
17use error::GitErrorInner;
18pub use error::{GitError, GitUrlParseError};
19
20#[derive(Clone, Debug)]
21pub struct GitUrl(Url);
22
23impl fmt::Display for GitUrl {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        fmt::Display::fmt(&self.0, f)
26    }
27}
28
29impl FromStr for GitUrl {
30    type Err = GitUrlParseError;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        Url::try_from(s).map(Self).map_err(GitUrlParseError)
34    }
35}
36
37#[derive(Debug)]
38pub struct Repository(gix::ThreadSafeRepository);
39
40impl Repository {
41    fn prepare_fetch(
42        url: GitUrl,
43        path: &Path,
44        kind: create::Kind,
45    ) -> Result<clone::PrepareFetch, GitErrorInner> {
46        Ok(clone::PrepareFetch::new(
47            url.0,
48            path,
49            kind,
50            create::Options {
51                destination_must_be_empty: true,
52                ..Default::default()
53            },
54            open::Options::default().permissions(Permissions::all()),
55        )?
56        .with_shallow(remote::fetch::Shallow::DepthAtRemote(
57            NonZeroU32::new(1).unwrap(),
58        )))
59    }
60
61    /// WARNING: This is a blocking operation, if you want to use it in
62    /// async context then you must wrap the call in [`tokio::task::spawn_blocking`].
63    ///
64    /// WARNING: This function must be called after tokio runtime is initialized.
65    pub fn shallow_clone_bare(
66        url: GitUrl,
67        path: &Path,
68        cancellation_token: Option<GitCancellationToken>,
69    ) -> Result<Self, GitError> {
70        debug!("Shallow cloning {url} to {}", path.display());
71
72        Ok(Self(
73            Self::prepare_fetch(url, path, create::Kind::Bare)?
74                .fetch_only(
75                    &mut TracingProgress::new("Cloning bare"),
76                    cancellation_token
77                        .as_ref()
78                        .map(GitCancellationToken::get_atomic)
79                        .unwrap_or(&AtomicBool::new(false)),
80                )
81                .map_err(GitErrorInner::from)?
82                .0
83                .into(),
84        ))
85    }
86
87    /// WARNING: This is a blocking operation, if you want to use it in
88    /// async context then you must wrap the call in [`tokio::task::spawn_blocking`].
89    ///
90    /// WARNING: This function must be called after tokio runtime is initialized.
91    pub fn shallow_clone(
92        url: GitUrl,
93        path: &Path,
94        cancellation_token: Option<GitCancellationToken>,
95    ) -> Result<Self, GitError> {
96        debug!("Shallow cloning {url} to {} with worktree", path.display());
97
98        let mut progress = TracingProgress::new("Cloning with worktree");
99
100        Ok(Self(
101            Self::prepare_fetch(url, path, create::Kind::WithWorktree)?
102                .fetch_then_checkout(&mut progress, &AtomicBool::new(false))
103                .map_err(GitErrorInner::from)?
104                .0
105                .main_worktree(
106                    &mut progress,
107                    cancellation_token
108                        .as_ref()
109                        .map(GitCancellationToken::get_atomic)
110                        .unwrap_or(&AtomicBool::new(false)),
111                )
112                .map_err(GitErrorInner::from)?
113                .0
114                .into(),
115        ))
116    }
117
118    #[inline(always)]
119    pub fn get_head_commit_entry_data_by_path(
120        &self,
121        path: impl AsRef<Path>,
122    ) -> Result<Option<Vec<u8>>, GitError> {
123        fn inner(this: &Repository, path: &Path) -> Result<Option<Vec<u8>>, GitErrorInner> {
124            Ok(
125                if let Some(entry) = this
126                    .0
127                    .to_thread_local()
128                    .head_commit()?
129                    .tree()?
130                    .peel_to_entry_by_path(path)?
131                {
132                    Some(mem::take(&mut entry.object()?.data))
133                } else {
134                    None
135                },
136            )
137        }
138
139        Ok(inner(self, path.as_ref())?)
140    }
141
142    pub fn get_head_commit_hash(&self) -> Result<impl fmt::Display, GitError> {
143        Ok(self
144            .0
145            .to_thread_local()
146            .head_commit()
147            .map_err(GitErrorInner::from)?
148            .id)
149    }
150}