1pub use crate::github::GitHubRepository;
2pub use crate::oid::{GitOid, OidParseError};
3pub use crate::reference::GitReference;
4
5use thiserror::Error;
6use uv_redacted::DisplaySafeUrl;
7
8mod github;
9mod oid;
10mod reference;
11
12#[derive(Debug, Error)]
13pub enum GitUrlParseError {
14 #[error(
15 "Unsupported Git URL scheme `{0}:` in `{1}` (expected one of `https:`, `ssh:`, or `file:`)"
16 )]
17 UnsupportedGitScheme(String, DisplaySafeUrl),
18}
19
20#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Hash, Ord)]
22pub struct GitUrl {
23 repository: DisplaySafeUrl,
26 reference: GitReference,
28 precise: Option<GitOid>,
30}
31
32impl GitUrl {
33 pub fn from_reference(
35 repository: DisplaySafeUrl,
36 reference: GitReference,
37 ) -> Result<Self, GitUrlParseError> {
38 Self::from_fields(repository, reference, None)
39 }
40
41 pub fn from_commit(
43 repository: DisplaySafeUrl,
44 reference: GitReference,
45 precise: GitOid,
46 ) -> Result<Self, GitUrlParseError> {
47 Self::from_fields(repository, reference, Some(precise))
48 }
49
50 pub fn from_fields(
52 repository: DisplaySafeUrl,
53 reference: GitReference,
54 precise: Option<GitOid>,
55 ) -> Result<Self, GitUrlParseError> {
56 match repository.scheme() {
57 "http" | "https" | "ssh" | "file" => {}
58 unsupported => {
59 return Err(GitUrlParseError::UnsupportedGitScheme(
60 unsupported.to_string(),
61 repository,
62 ));
63 }
64 }
65 Ok(Self {
66 repository,
67 reference,
68 precise,
69 })
70 }
71
72 #[must_use]
74 pub fn with_precise(mut self, precise: GitOid) -> Self {
75 self.precise = Some(precise);
76 self
77 }
78
79 #[must_use]
81 pub fn with_reference(mut self, reference: GitReference) -> Self {
82 self.reference = reference;
83 self
84 }
85
86 pub fn repository(&self) -> &DisplaySafeUrl {
88 &self.repository
89 }
90
91 pub fn reference(&self) -> &GitReference {
93 &self.reference
94 }
95
96 pub fn precise(&self) -> Option<GitOid> {
98 self.precise
99 }
100}
101
102impl TryFrom<DisplaySafeUrl> for GitUrl {
103 type Error = GitUrlParseError;
104
105 fn try_from(mut url: DisplaySafeUrl) -> Result<Self, Self::Error> {
107 url.set_fragment(None);
109 url.set_query(None);
110
111 let mut reference = GitReference::DefaultBranch;
114 if let Some((prefix, suffix)) = url
115 .path()
116 .rsplit_once('@')
117 .map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string()))
118 {
119 reference = GitReference::from_rev(suffix);
120 url.set_path(&prefix);
121 }
122
123 Self::from_reference(url, reference)
124 }
125}
126
127impl From<GitUrl> for DisplaySafeUrl {
128 fn from(git: GitUrl) -> Self {
129 let mut url = git.repository;
130
131 if let Some(precise) = git.precise {
133 let path = format!("{}@{}", url.path(), precise);
134 url.set_path(&path);
135 } else {
136 match git.reference {
138 GitReference::Branch(rev)
139 | GitReference::Tag(rev)
140 | GitReference::BranchOrTag(rev)
141 | GitReference::NamedRef(rev)
142 | GitReference::BranchOrTagOrCommit(rev) => {
143 let path = format!("{}@{}", url.path(), rev);
144 url.set_path(&path);
145 }
146 GitReference::DefaultBranch => {}
147 }
148 }
149
150 url
151 }
152}
153
154impl std::fmt::Display for GitUrl {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(f, "{}", &self.repository)
157 }
158}