oro_package_spec/
gitinfo.rs

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// TODO: impl FromStr? We already have a parser, just need to hook it up.
59#[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}