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 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 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}