1pub use crate::github::GitHubRepository;
2pub use crate::oid::{GitOid, OidParseError};
3pub use crate::reference::GitReference;
4use std::cmp::Ordering;
5use std::sync::LazyLock;
6
7use thiserror::Error;
8use uv_cache_key::RepositoryUrl;
9use uv_redacted::DisplaySafeUrl;
10use uv_static::EnvVars;
11
12mod github;
13mod oid;
14mod reference;
15
16pub static UV_GIT_LFS: LazyLock<GitLfs> = LazyLock::new(|| {
18 if std::env::var_os(EnvVars::UV_GIT_LFS)
20 .and_then(|v| v.to_str().map(str::to_lowercase))
21 .is_some_and(|v| matches!(v.as_str(), "y" | "yes" | "t" | "true" | "on" | "1"))
22 {
23 GitLfs::Enabled
24 } else {
25 GitLfs::Disabled
26 }
27});
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
31pub enum GitLfs {
32 #[default]
34 Disabled,
35 Enabled,
37}
38
39impl GitLfs {
40 pub fn from_env() -> Self {
42 *UV_GIT_LFS
43 }
44
45 pub fn enabled(self) -> bool {
47 matches!(self, Self::Enabled)
48 }
49}
50
51impl From<Option<bool>> for GitLfs {
52 fn from(value: Option<bool>) -> Self {
53 match value {
54 Some(true) => Self::Enabled,
55 Some(false) => Self::Disabled,
56 None => Self::from_env(),
57 }
58 }
59}
60
61impl From<bool> for GitLfs {
62 fn from(value: bool) -> Self {
63 if value { Self::Enabled } else { Self::Disabled }
64 }
65}
66
67impl std::fmt::Display for GitLfs {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::Enabled => write!(f, "enabled"),
71 Self::Disabled => write!(f, "disabled"),
72 }
73 }
74}
75
76#[derive(Debug, Error)]
77pub enum GitUrlParseError {
78 #[error(
79 "Unsupported Git URL scheme `{0}:` in `{1}` (expected one of `https:`, `ssh:`, or `file:`)"
80 )]
81 UnsupportedGitScheme(String, DisplaySafeUrl),
82}
83
84#[derive(Debug, Clone)]
86pub struct GitUrl {
87 url: DisplaySafeUrl,
90 repository: RepositoryUrl,
92 reference: GitReference,
94 precise: Option<GitOid>,
96 lfs: GitLfs,
98}
99
100impl GitUrl {
101 pub fn from_reference(
103 url: DisplaySafeUrl,
104 reference: GitReference,
105 lfs: GitLfs,
106 ) -> Result<Self, GitUrlParseError> {
107 Self::from_fields(url, reference, None, lfs)
108 }
109
110 pub fn from_commit(
112 url: DisplaySafeUrl,
113 reference: GitReference,
114 precise: GitOid,
115 lfs: GitLfs,
116 ) -> Result<Self, GitUrlParseError> {
117 Self::from_fields(url, reference, Some(precise), lfs)
118 }
119
120 pub fn from_fields(
122 url: DisplaySafeUrl,
123 reference: GitReference,
124 precise: Option<GitOid>,
125 lfs: GitLfs,
126 ) -> Result<Self, GitUrlParseError> {
127 match url.scheme() {
128 "http" | "https" | "ssh" | "file" => {}
129 unsupported => {
130 return Err(GitUrlParseError::UnsupportedGitScheme(
131 unsupported.to_string(),
132 url,
133 ));
134 }
135 }
136 Ok(Self {
137 repository: RepositoryUrl::new(&url),
138 url,
139 reference,
140 precise,
141 lfs,
142 })
143 }
144
145 #[must_use]
147 pub fn with_precise(mut self, precise: GitOid) -> Self {
148 self.precise = Some(precise);
149 self
150 }
151
152 #[must_use]
154 pub fn with_reference(mut self, reference: GitReference) -> Self {
155 self.reference = reference;
156 self
157 }
158
159 pub fn url(&self) -> &DisplaySafeUrl {
161 &self.url
162 }
163
164 pub fn repository(&self) -> &RepositoryUrl {
166 &self.repository
167 }
168
169 pub fn reference(&self) -> &GitReference {
171 &self.reference
172 }
173
174 pub fn precise(&self) -> Option<GitOid> {
176 self.precise
177 }
178
179 pub fn lfs(&self) -> GitLfs {
181 self.lfs
182 }
183
184 #[must_use]
186 pub fn with_lfs(mut self, lfs: GitLfs) -> Self {
187 self.lfs = lfs;
188 self
189 }
190}
191
192impl PartialEq for GitUrl {
193 fn eq(&self, other: &Self) -> bool {
194 self.repository == other.repository
195 && self.reference == other.reference
196 && self.precise == other.precise
197 && self.lfs == other.lfs
198 }
199}
200
201impl Eq for GitUrl {}
202
203impl PartialOrd for GitUrl {
204 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
205 Some(self.cmp(other))
206 }
207}
208
209impl Ord for GitUrl {
210 fn cmp(&self, other: &Self) -> Ordering {
211 self.repository
212 .cmp(&other.repository)
213 .then_with(|| self.reference.cmp(&other.reference))
214 .then_with(|| self.precise.cmp(&other.precise))
215 .then_with(|| self.lfs.cmp(&other.lfs))
216 }
217}
218
219impl std::hash::Hash for GitUrl {
220 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
221 self.repository.hash(state);
222 self.reference.hash(state);
223 self.precise.hash(state);
224 self.lfs.hash(state);
225 }
226}
227
228impl TryFrom<DisplaySafeUrl> for GitUrl {
229 type Error = GitUrlParseError;
230
231 fn try_from(mut url: DisplaySafeUrl) -> Result<Self, Self::Error> {
233 url.set_fragment(None);
235 url.set_query(None);
236
237 let mut reference = GitReference::DefaultBranch;
240 if let Some((prefix, suffix)) = url
241 .path()
242 .rsplit_once('@')
243 .map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string()))
244 {
245 reference = GitReference::from_rev(suffix);
246 url.set_path(&prefix);
247 }
248
249 Self::from_reference(url, reference, GitLfs::from_env())
251 }
252}
253
254impl From<GitUrl> for DisplaySafeUrl {
255 fn from(git: GitUrl) -> Self {
256 let mut url = git.url;
257
258 if let Some(precise) = git.precise {
260 let path = format!("{}@{}", url.path(), precise);
261 url.set_path(&path);
262 } else {
263 match git.reference {
265 GitReference::Branch(rev)
266 | GitReference::Tag(rev)
267 | GitReference::BranchOrTag(rev)
268 | GitReference::NamedRef(rev)
269 | GitReference::BranchOrTagOrCommit(rev) => {
270 let path = format!("{}@{}", url.path(), rev);
271 url.set_path(&path);
272 }
273 GitReference::DefaultBranch => {}
274 }
275 }
276
277 url
278 }
279}
280
281impl std::fmt::Display for GitUrl {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 write!(f, "{}", &self.url)
284 }
285}