uv_git_types/
lib.rs

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
14/// Initialize [`GitLfs`] mode from `UV_GIT_LFS` environment.
15pub static UV_GIT_LFS: LazyLock<GitLfs> = LazyLock::new(|| {
16    // TODO(konsti): Parse this in `EnvironmentOptions`.
17    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/// Configuration for Git LFS (Large File Storage) support.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
29pub enum GitLfs {
30    /// Git LFS is disabled (default).
31    #[default]
32    Disabled,
33    /// Git LFS is enabled.
34    Enabled,
35}
36
37impl GitLfs {
38    /// Create a `GitLfs` configuration from environment variables.
39    pub fn from_env() -> Self {
40        *UV_GIT_LFS
41    }
42
43    /// Returns true if LFS is enabled.
44    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/// A URL reference to a Git repository.
83#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Hash, Ord)]
84pub struct GitUrl {
85    /// The URL of the Git repository, with any query parameters, fragments, and leading `git+`
86    /// removed.
87    repository: DisplaySafeUrl,
88    /// The reference to the commit to use, which could be a branch, tag or revision.
89    reference: GitReference,
90    /// The precise commit to use, if known.
91    precise: Option<GitOid>,
92    /// Git LFS configuration for this repository.
93    lfs: GitLfs,
94}
95
96impl GitUrl {
97    /// Create a new [`GitUrl`] from a repository URL and a reference.
98    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    /// Create a new [`GitUrl`] from a repository URL and a precise commit.
107    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    /// Create a new [`GitUrl`] from a repository URL and a precise commit, if known.
117    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    /// Set the precise [`GitOid`] to use for this Git URL.
141    #[must_use]
142    pub fn with_precise(mut self, precise: GitOid) -> Self {
143        self.precise = Some(precise);
144        self
145    }
146
147    /// Set the [`GitReference`] to use for this Git URL.
148    #[must_use]
149    pub fn with_reference(mut self, reference: GitReference) -> Self {
150        self.reference = reference;
151        self
152    }
153
154    /// Return the [`Url`] of the Git repository.
155    pub fn repository(&self) -> &DisplaySafeUrl {
156        &self.repository
157    }
158
159    /// Return the reference to the commit to use, which could be a branch, tag or revision.
160    pub fn reference(&self) -> &GitReference {
161        &self.reference
162    }
163
164    /// Return the precise commit, if known.
165    pub fn precise(&self) -> Option<GitOid> {
166        self.precise
167    }
168
169    /// Return the Git LFS configuration.
170    pub fn lfs(&self) -> GitLfs {
171        self.lfs
172    }
173
174    /// Set the Git LFS configuration.
175    #[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    /// Initialize a [`GitUrl`] source from a URL.
186    fn try_from(mut url: DisplaySafeUrl) -> Result<Self, Self::Error> {
187        // Remove any query parameters and fragments.
188        url.set_fragment(None);
189        url.set_query(None);
190
191        // If the URL ends with a reference, like `https://git.example.com/MyProject.git@v1.0`,
192        // extract it.
193        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        // TODO(samypr100): GitLfs::from_env() for now unless we want to support parsing lfs=true
204        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 we have a precise commit, add `@` and the commit hash to the URL.
213        if let Some(precise) = git.precise {
214            let path = format!("{}@{}", url.path(), precise);
215            url.set_path(&path);
216        } else {
217            // Otherwise, add the branch or tag name.
218            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}