Skip to main content

uv_git_types/
lib.rs

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
16/// Initialize [`GitLfs`] mode from `UV_GIT_LFS` environment.
17pub static UV_GIT_LFS: LazyLock<GitLfs> = LazyLock::new(|| {
18    // TODO(konsti): Parse this in `EnvironmentOptions`.
19    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/// Configuration for Git LFS (Large File Storage) support.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
31pub enum GitLfs {
32    /// Git LFS is disabled (default).
33    #[default]
34    Disabled,
35    /// Git LFS is enabled.
36    Enabled,
37}
38
39impl GitLfs {
40    /// Create a `GitLfs` configuration from environment variables.
41    pub fn from_env() -> Self {
42        *UV_GIT_LFS
43    }
44
45    /// Returns true if LFS is enabled.
46    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/// A URL reference to a Git repository.
85#[derive(Debug, Clone)]
86pub struct GitUrl {
87    /// The URL of the Git repository, with any query parameters, fragments, and leading `git+`
88    /// removed.
89    url: DisplaySafeUrl,
90    /// The canonical repository identity used for comparison and hashing.
91    repository: RepositoryUrl,
92    /// The reference to the commit to use, which could be a branch, tag or revision.
93    reference: GitReference,
94    /// The precise commit to use, if known.
95    precise: Option<GitOid>,
96    /// Git LFS configuration for this repository.
97    lfs: GitLfs,
98}
99
100impl GitUrl {
101    /// Create a new [`GitUrl`] from a repository URL and a reference.
102    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    /// Create a new [`GitUrl`] from a repository URL and a precise commit.
111    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    /// Create a new [`GitUrl`] from a repository URL and a precise commit, if known.
121    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    /// Set the precise [`GitOid`] to use for this Git URL.
146    #[must_use]
147    pub fn with_precise(mut self, precise: GitOid) -> Self {
148        self.precise = Some(precise);
149        self
150    }
151
152    /// Set the [`GitReference`] to use for this Git URL.
153    #[must_use]
154    pub fn with_reference(mut self, reference: GitReference) -> Self {
155        self.reference = reference;
156        self
157    }
158
159    /// Return the [`Url`] of the Git repository.
160    pub fn url(&self) -> &DisplaySafeUrl {
161        &self.url
162    }
163
164    /// Return the canonical repository identity for this Git URL.
165    pub fn repository(&self) -> &RepositoryUrl {
166        &self.repository
167    }
168
169    /// Return the reference to the commit to use, which could be a branch, tag or revision.
170    pub fn reference(&self) -> &GitReference {
171        &self.reference
172    }
173
174    /// Return the precise commit, if known.
175    pub fn precise(&self) -> Option<GitOid> {
176        self.precise
177    }
178
179    /// Return the Git LFS configuration.
180    pub fn lfs(&self) -> GitLfs {
181        self.lfs
182    }
183
184    /// Set the Git LFS configuration.
185    #[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    /// Initialize a [`GitUrl`] source from a URL.
232    fn try_from(mut url: DisplaySafeUrl) -> Result<Self, Self::Error> {
233        // Remove any query parameters and fragments.
234        url.set_fragment(None);
235        url.set_query(None);
236
237        // If the URL ends with a reference, like `https://git.example.com/MyProject.git@v1.0`,
238        // extract it.
239        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        // TODO(samypr100): GitLfs::from_env() for now unless we want to support parsing lfs=true
250        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 we have a precise commit, add `@` and the commit hash to the URL.
259        if let Some(precise) = git.precise {
260            let path = format!("{}@{}", url.path(), precise);
261            url.set_path(&path);
262        } else {
263            // Otherwise, add the branch or tag name.
264            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}