1pub use crate::github::GitHubRepository;
2pub use crate::oid::{GitOid, OidParseError};
3pub use crate::reference::GitReference;
4use std::sync::LazyLock;
5
6use thiserror::Error;
7use uv_redacted::DisplaySafeUrl;
8use uv_static::EnvVars;
9
10mod github;
11mod oid;
12mod reference;
13
14pub static UV_GIT_LFS: LazyLock<GitLfs> = LazyLock::new(|| {
16 if std::env::var_os(EnvVars::UV_GIT_LFS)
18 .and_then(|v| v.to_str().map(str::to_lowercase))
19 .is_some_and(|v| matches!(v.as_str(), "y" | "yes" | "t" | "true" | "on" | "1"))
20 {
21 GitLfs::Enabled
22 } else {
23 GitLfs::Disabled
24 }
25});
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
29pub enum GitLfs {
30 #[default]
32 Disabled,
33 Enabled,
35}
36
37impl GitLfs {
38 pub fn from_env() -> Self {
40 *UV_GIT_LFS
41 }
42
43 pub fn enabled(self) -> bool {
45 matches!(self, Self::Enabled)
46 }
47}
48
49impl From<Option<bool>> for GitLfs {
50 fn from(value: Option<bool>) -> Self {
51 match value {
52 Some(true) => Self::Enabled,
53 Some(false) => Self::Disabled,
54 None => Self::from_env(),
55 }
56 }
57}
58
59impl From<bool> for GitLfs {
60 fn from(value: bool) -> Self {
61 if value { Self::Enabled } else { Self::Disabled }
62 }
63}
64
65impl std::fmt::Display for GitLfs {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Self::Enabled => write!(f, "enabled"),
69 Self::Disabled => write!(f, "disabled"),
70 }
71 }
72}
73
74#[derive(Debug, Error)]
75pub enum GitUrlParseError {
76 #[error(
77 "Unsupported Git URL scheme `{0}:` in `{1}` (expected one of `https:`, `ssh:`, or `file:`)"
78 )]
79 UnsupportedGitScheme(String, DisplaySafeUrl),
80}
81
82#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Hash, Ord)]
84pub struct GitUrl {
85 repository: DisplaySafeUrl,
88 reference: GitReference,
90 precise: Option<GitOid>,
92 lfs: GitLfs,
94}
95
96impl GitUrl {
97 pub fn from_reference(
99 repository: DisplaySafeUrl,
100 reference: GitReference,
101 lfs: GitLfs,
102 ) -> Result<Self, GitUrlParseError> {
103 Self::from_fields(repository, reference, None, lfs)
104 }
105
106 pub fn from_commit(
108 repository: DisplaySafeUrl,
109 reference: GitReference,
110 precise: GitOid,
111 lfs: GitLfs,
112 ) -> Result<Self, GitUrlParseError> {
113 Self::from_fields(repository, reference, Some(precise), lfs)
114 }
115
116 pub fn from_fields(
118 repository: DisplaySafeUrl,
119 reference: GitReference,
120 precise: Option<GitOid>,
121 lfs: GitLfs,
122 ) -> Result<Self, GitUrlParseError> {
123 match repository.scheme() {
124 "http" | "https" | "ssh" | "file" => {}
125 unsupported => {
126 return Err(GitUrlParseError::UnsupportedGitScheme(
127 unsupported.to_string(),
128 repository,
129 ));
130 }
131 }
132 Ok(Self {
133 repository,
134 reference,
135 precise,
136 lfs,
137 })
138 }
139
140 #[must_use]
142 pub fn with_precise(mut self, precise: GitOid) -> Self {
143 self.precise = Some(precise);
144 self
145 }
146
147 #[must_use]
149 pub fn with_reference(mut self, reference: GitReference) -> Self {
150 self.reference = reference;
151 self
152 }
153
154 pub fn repository(&self) -> &DisplaySafeUrl {
156 &self.repository
157 }
158
159 pub fn reference(&self) -> &GitReference {
161 &self.reference
162 }
163
164 pub fn precise(&self) -> Option<GitOid> {
166 self.precise
167 }
168
169 pub fn lfs(&self) -> GitLfs {
171 self.lfs
172 }
173
174 #[must_use]
176 pub fn with_lfs(mut self, lfs: GitLfs) -> Self {
177 self.lfs = lfs;
178 self
179 }
180}
181
182impl TryFrom<DisplaySafeUrl> for GitUrl {
183 type Error = GitUrlParseError;
184
185 fn try_from(mut url: DisplaySafeUrl) -> Result<Self, Self::Error> {
187 url.set_fragment(None);
189 url.set_query(None);
190
191 let mut reference = GitReference::DefaultBranch;
194 if let Some((prefix, suffix)) = url
195 .path()
196 .rsplit_once('@')
197 .map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string()))
198 {
199 reference = GitReference::from_rev(suffix);
200 url.set_path(&prefix);
201 }
202
203 Self::from_reference(url, reference, GitLfs::from_env())
205 }
206}
207
208impl From<GitUrl> for DisplaySafeUrl {
209 fn from(git: GitUrl) -> Self {
210 let mut url = git.repository;
211
212 if let Some(precise) = git.precise {
214 let path = format!("{}@{}", url.path(), precise);
215 url.set_path(&path);
216 } else {
217 match git.reference {
219 GitReference::Branch(rev)
220 | GitReference::Tag(rev)
221 | GitReference::BranchOrTag(rev)
222 | GitReference::NamedRef(rev)
223 | GitReference::BranchOrTagOrCommit(rev) => {
224 let path = format!("{}@{}", url.path(), rev);
225 url.set_path(&path);
226 }
227 GitReference::DefaultBranch => {}
228 }
229 }
230
231 url
232 }
233}
234
235impl std::fmt::Display for GitUrl {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 write!(f, "{}", &self.repository)
238 }
239}