1use std::fmt;
2use std::str::FromStr;
3
4use node_semver::Range;
5use nom::combinator::all_consuming;
6use nom::Err;
7use url::Url;
8
9use crate::error::{PackageSpecError, SpecErrorKind};
10use crate::parsers::git;
11use crate::PackageSpec;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum GitHost {
15 GitHub,
16 Gist,
17 GitLab,
18 Bitbucket,
19}
20
21impl FromStr for GitHost {
22 type Err = PackageSpecError;
23
24 fn from_str(s: &str) -> Result<Self, Self::Err> {
25 Ok(match s.to_lowercase().as_str() {
26 "github" => GitHost::GitHub,
27 "gist" => GitHost::Gist,
28 "gitlab" => GitHost::GitLab,
29 "bitbucket" => GitHost::Bitbucket,
30 _ => {
31 return Err(PackageSpecError {
32 input: s.into(),
33 offset: 0,
34 kind: SpecErrorKind::InvalidGitHost(s.into()),
35 })
36 }
37 })
38 }
39}
40
41impl fmt::Display for GitHost {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 use GitHost::*;
44 write!(
45 f,
46 "{}",
47 match self {
48 GitHub => "github",
49 Gist => "gist",
50 GitLab => "gitlab",
51 Bitbucket => "bitbucket",
52 }
53 )?;
54 Ok(())
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60pub enum GitInfo {
61 Hosted {
62 owner: String,
63 repo: String,
64 host: GitHost,
65 committish: Option<String>,
66 semver: Option<Range>,
67 requested: Option<String>,
68 },
69 Url {
70 url: Url,
71 committish: Option<String>,
72 semver: Option<Range>,
73 },
74 Ssh {
75 ssh: String,
76 committish: Option<String>,
77 semver: Option<Range>,
78 },
79}
80
81impl GitInfo {
82 pub fn committish(&self) -> Option<&str> {
83 use GitInfo::*;
84 match self {
85 Hosted { committish, .. } => committish.as_deref(),
86 Url { committish, .. } => committish.as_deref(),
87 Ssh { committish, .. } => committish.as_deref(),
88 }
89 }
90
91 pub fn semver(&self) -> Option<&Range> {
92 use GitInfo::*;
93 match self {
94 Hosted { semver, .. } => semver.as_ref(),
95 Url { semver, .. } => semver.as_ref(),
96 Ssh { semver, .. } => semver.as_ref(),
97 }
98 }
99
100 pub fn ssh(&self) -> Option<String> {
101 use GitHost::*;
102 use GitInfo::*;
103 match self {
104 GitInfo::Url { .. } | Ssh { .. } => None,
105 Hosted {
106 ref host,
107 ref owner,
108 ref repo,
109 ..
110 } => Some(match host {
111 GitHub => format!("git@github.com:{owner}/{repo}.git"),
112 Gist => format!("git@gist.github.com:/{repo}"),
113 GitLab => format!("git@gitlab.com:{owner}/{repo}.git"),
114 Bitbucket => format!("git@bitbucket.com:{owner}/{repo}"),
115 })
116 .map(|url| url.parse().expect("URL failed to parse")),
117 }
118 }
119
120 pub fn https(&self) -> Option<Url> {
121 use GitHost::*;
122 use GitInfo::*;
123 match self {
124 GitInfo::Url { .. } | Ssh { .. } => None,
125 Hosted {
126 ref host,
127 ref owner,
128 ref repo,
129 ..
130 } => Some(match host {
131 GitHub => format!("https://github.com/{owner}/{repo}.git"),
132 Gist => format!("https://gist.github.com/{repo}.git"),
133 GitLab => format!("https://gitlab.com/{owner}/{repo}.git"),
134 Bitbucket => format!("https://bitbucket.com/{owner}/{repo}.git"),
135 })
136 .map(|url| url.parse().expect("URL failed to parse")),
137 }
138 }
139
140 pub fn tarball(&self) -> Option<Url> {
141 use GitHost::*;
142 use GitInfo::*;
143 match self {
144 GitInfo::Url { .. } | Ssh { .. } => None,
145 Hosted {
146 ref host,
147 ref owner,
148 ref repo,
149 ref committish,
150 ..
151 } => committish
152 .as_ref()
153 .map(|commit| match host {
154 GitHub => format!("https://codeload.github.com/{owner}/{repo}/tag.gz/{commit}"),
155 Gist => format!("https://codeload.github.com/gist/{repo}/tar.gz/{commit}"),
156 GitLab => format!(
157 "https://gitlan.com/{owner}/{repo}/repository/archive.tar.gz?ref={commit}"
158 ),
159 Bitbucket => {
160 format!("https://bitbucket.org/{owner}/{repo}/get/{commit}.tar.gz")
161 }
162 })
163 .map(|url| url.parse().expect("Failed to parse URL.")),
164 }
165 }
166}
167
168impl fmt::Display for GitInfo {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 use GitInfo::*;
171 match self {
172 GitInfo::Url {
173 url,
174 committish,
175 semver,
176 } => {
177 if url.scheme() != "git" {
178 write!(f, "git+")?;
179 }
180 write!(f, "{url}")?;
181 if let Some(comm) = committish {
182 write!(f, "#{comm}")?;
183 } else if let Some(semver) = semver {
184 write!(f, "#semver:{semver}")?;
185 }
186 }
187 Ssh {
188 ssh,
189 committish,
190 semver,
191 } => {
192 write!(f, "git+ssh://{ssh}")?;
193 if let Some(comm) = committish {
194 write!(f, "#{comm}")?;
195 } else if let Some(semver) = semver {
196 write!(f, "#semver:{semver}")?;
197 }
198 }
199 Hosted {
200 requested,
201 owner,
202 repo,
203 host,
204 committish,
205 semver,
206 } => {
207 if let Some(requested) = requested {
208 if !requested.starts_with("git://") {
209 write!(f, "git+")?;
210 }
211 write!(f, "{requested}")?;
212 } else {
213 write!(f, "{host}:{owner}/{repo}")?;
214 }
215
216 if let Some(comm) = committish {
217 write!(f, "#{comm}")?;
218 } else if let Some(semver) = semver {
219 write!(f, "#semver:{semver}")?;
220 }
221 }
222 }
223 Ok(())
224 }
225}
226
227impl FromStr for GitInfo {
228 type Err = PackageSpecError;
229
230 fn from_str(s: &str) -> Result<Self, Self::Err> {
231 parse_gitinfo(s)
232 }
233}
234
235fn parse_gitinfo<I>(input: I) -> Result<GitInfo, PackageSpecError>
236where
237 I: AsRef<str>,
238{
239 let input = input.as_ref();
240 match all_consuming(git::git_spec)(input) {
241 Ok((_, PackageSpec::Git(arg))) => Ok(arg),
242 Ok(_) => unreachable!("This should only return git specs"),
243 Err(err) => Err(match err {
244 Err::Error(e) | Err::Failure(e) => PackageSpecError {
245 input: input.into(),
246 offset: e.input.as_ptr() as usize - input.as_ptr() as usize,
247 kind: if let Some(kind) = e.kind {
248 kind
249 } else if let Some(ctx) = e.context {
250 SpecErrorKind::Context(ctx)
251 } else {
252 SpecErrorKind::Other
253 },
254 },
255 Err::Incomplete(_) => PackageSpecError {
256 input: input.into(),
257 offset: input.len() - 1,
258 kind: SpecErrorKind::IncompleteInput,
259 },
260 }),
261 }
262}
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn from_str() {
269 let info_url = GitInfo::Url {
270 url: "https://foo.com/hello.git".parse().unwrap(),
271 committish: Some("deadbeef".into()),
272 semver: None,
273 };
274 let parsed_url: GitInfo = "git+https://foo.com/hello.git#deadbeef".parse().unwrap();
275 assert_eq!(parsed_url, info_url);
276
277 let info_ssh = GitInfo::Ssh {
278 ssh: "git@foo.com:here.git".into(),
279 committish: None,
280 semver: Some("^1.2.3".parse().unwrap()),
281 };
282 let parsed_ssh: GitInfo = "git+ssh://git@foo.com:here.git#semver:>=1.2.3 <2.0.0-0"
283 .parse()
284 .unwrap();
285 assert_eq!(parsed_ssh, info_ssh);
286
287 let info_hosted = GitInfo::Hosted {
288 owner: "foo".into(),
289 repo: "bar".into(),
290 host: GitHost::GitHub,
291 committish: None,
292 semver: None,
293 requested: None,
294 };
295 let parsed_hosted: GitInfo = "github:foo/bar".parse().unwrap();
296 assert_eq!(parsed_hosted, info_hosted);
297 }
298
299 #[test]
300 fn display_url() {
301 let info = GitInfo::Url {
302 url: "https://foo.com/hello.git".parse().unwrap(),
303 committish: Some("deadbeef".into()),
304 semver: None,
305 };
306 assert_eq!(
307 String::from("git+https://foo.com/hello.git#deadbeef"),
308 format!("{info}")
309 );
310 let info = GitInfo::Url {
311 url: "git://foo.org/goodbye.git".parse().unwrap(),
312 committish: None,
313 semver: Some("^1.2.3".parse().unwrap()),
314 };
315 assert_eq!(
316 String::from("git://foo.org/goodbye.git#semver:>=1.2.3 <2.0.0-0"),
317 format!("{info}")
318 );
319 }
320
321 #[test]
322 fn display_ssh() {
323 let info = GitInfo::Ssh {
324 ssh: "git@foo.com:here.git".into(),
325 committish: Some("deadbeef".into()),
326 semver: None,
327 };
328 assert_eq!(
329 String::from("git+ssh://git@foo.com:here.git#deadbeef"),
330 format!("{info}")
331 );
332 let info = GitInfo::Ssh {
333 ssh: "git@foo.com:here.git".into(),
334 committish: None,
335 semver: Some("^1.2.3".parse().unwrap()),
336 };
337 assert_eq!(
338 String::from("git+ssh://git@foo.com:here.git#semver:>=1.2.3 <2.0.0-0"),
339 format!("{info}")
340 );
341 }
342
343 #[test]
344 fn display_hosted() {
345 let info = GitInfo::Hosted {
346 owner: "foo".into(),
347 repo: "bar".into(),
348 host: GitHost::GitHub,
349 committish: None,
350 semver: None,
351 requested: None,
352 };
353 assert_eq!(String::from("github:foo/bar"), format!("{info}"));
354 let info = GitInfo::Hosted {
355 owner: "foo".into(),
356 repo: "bar".into(),
357 host: GitHost::GitHub,
358 committish: Some("deadbeef".into()),
359 semver: None,
360 requested: Some("https://github.com/foo/bar.git".into()),
361 };
362 assert_eq!(
363 String::from("git+https://github.com/foo/bar.git#deadbeef"),
364 format!("{info}")
365 );
366 let info = GitInfo::Hosted {
367 owner: "foo".into(),
368 repo: "bar".into(),
369 host: GitHost::GitHub,
370 committish: Some("deadbeef".into()),
371 semver: None,
372 requested: Some("git://gitlab.com/foo/bar.git".into()),
373 };
374 assert_eq!(
375 String::from("git://gitlab.com/foo/bar.git#deadbeef"),
376 format!("{info}")
377 );
378 }
379}