Skip to content

Commit 31088d9

Browse files
committed
Bump version to 0.2.0
* Added trim_url method to GitUrl - Resolves #2 * Rename Protocol to Scheme - Resolves #3 * Reimplement Display trait - Resolves #4 * Removed new() method from GitUrl * Update README.md with new example output * Update all tests
1 parent f26ccc9 commit 31088d9

File tree

8 files changed

+254
-65
lines changed

8 files changed

+254
-65
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ license = "MIT"
99
name = "git-url-parse"
1010
readme = "README.md"
1111
repository = "https://github.com/tjtelan/git-url-parse-rs"
12-
version = "0.1.1"
12+
version = "0.2.0"
1313

1414
[badges.maintenance]
1515
status = "actively-developed"

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Internally uses `Url::parse()` from the [Url](https://crates.io/crates/url) crat
2828

2929
```shell
3030
$ RUST_LOG=git_url_parse cargo run --example multi
31+
$ RUST_LOG=git_url_parse cargo run --example trim_auth
3132
```
3233

3334
### Simple usage and output
@@ -42,6 +43,6 @@ fn main() {
4243

4344
### Example Output
4445
```bash
45-
SSH: Ok(GitUrl { href: "[email protected]:tjtelan/git-url-parse-rs.git", host: Some("github.com"), name: "git-url-parse-rs", owner: Some("tjtelan"), organization: None, fullname: "tjtelan/git-url-parse-rs", protocol: Ssh, user: Some("git"), token: None, port: None, path: "tjtelan/git-url-parse-rs.git", git_suffix: true })
46-
HTTPS: Ok(GitUrl { href: "https://github.com/tjtelan/git-url-parse-rs", host: Some("github.com"), name: "git-url-parse-rs", owner: Some("tjtelan"), organization: None, fullname: "tjtelan/git-url-parse-rs", protocol: Https, user: None, token: None, port: None, path: "/tjtelan/git-url-parse-rs", git_suffix: false })
46+
SSH: Ok(GitUrl { host: Some("github.com"), name: "git-url-parse-rs", owner: Some("tjtelan"), organization: None, fullname: "tjtelan/git-url-parse-rs", scheme: Ssh, user: Some("git"), token: None, port: None, path: "tjtelan/git-url-parse-rs.git", git_suffix: true, scheme_prefix: false })
47+
HTTPS: Ok(GitUrl { host: Some("github.com"), name: "git-url-parse-rs", owner: Some("tjtelan"), organization: None, fullname: "tjtelan/git-url-parse-rs", scheme: Https, user: None, token: None, port: None, path: "/tjtelan/git-url-parse-rs", git_suffix: false, scheme_prefix: true })
4748
```

examples/multi.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
use anyhow::Result;
12
use git_url_parse::GitUrl;
23

3-
fn main() {
4+
fn main() -> Result<()> {
45
env_logger::init();
56

67
let test_vec = vec![
@@ -20,6 +21,10 @@ fn main() {
2021
];
2122

2223
for test_url in test_vec {
23-
println!("{:?}", GitUrl::parse(test_url));
24+
let parsed = GitUrl::parse(test_url)?;
25+
println!("Original: {}", test_url);
26+
println!("Parsed: {}", parsed);
27+
println!("{:?}\n", parsed);
2428
}
29+
Ok(())
2530
}

examples/trim_auth.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use anyhow::Result;
2+
use git_url_parse::GitUrl;
3+
4+
fn main() -> Result<()> {
5+
env_logger::init();
6+
7+
let test_vec = vec![
8+
"https://github.com/tjtelan/orbitalci.git",
9+
"[email protected]:tjtelan/orbitalci.git",
10+
"https://token:[email protected]/path/to/repo.git/",
11+
"https://x-token-auth:[email protected]/path/to/repo.git/",
12+
"git+ssh://[email protected]/and-the-path/name",
13+
"git://some-host.com/and-the-path/name",
14+
"host.tld:user/project-name.git",
15+
"file:///path/to/repo.git/",
16+
"~/path/to/repo.git/",
17+
"./path/to/repo.git/",
18+
"./path/to/repo.git",
19+
"[email protected]:v3/CompanyName/ProjectName/RepoName",
20+
"https://[email protected]/CompanyName/ProjectName/_git/RepoName",
21+
];
22+
23+
for test_url in test_vec {
24+
println!("Original: {}", test_url);
25+
println!(
26+
"Parsed + Trimmed: {}\n",
27+
GitUrl::parse(test_url)?.trim_auth()
28+
);
29+
}
30+
Ok(())
31+
}

src/lib.rs

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ use log::debug;
33
use regex::Regex;
44
use std::fmt;
55
use std::str::FromStr;
6-
use strum_macros::{EnumString, EnumVariantNames};
6+
use strum_macros::{Display, EnumString, EnumVariantNames};
77
use url::Url;
88

99
/// Supported uri schemes for parsing
10-
#[derive(Debug, PartialEq, EnumString, EnumVariantNames, Clone)]
10+
#[derive(Debug, PartialEq, EnumString, EnumVariantNames, Clone, Display, Copy)]
1111
#[strum(serialize_all = "kebab_case")]
12-
pub enum Protocol {
12+
pub enum Scheme {
1313
/// Represents No url scheme
1414
Unspecified,
1515
/// Represents `file://` url scheme
@@ -27,14 +27,12 @@ pub enum Protocol {
2727
GitSsh,
2828
}
2929

30-
/// GitUrl represents an input url `href` that is a url used by git
30+
/// GitUrl represents an input url that is a url used by git
3131
/// Internally during parsing the url is sanitized and uses the `url` crate to perform
3232
/// the majority of the parsing effort, and with some extra handling to expose
3333
/// metadata used my many git hosting services
3434
#[derive(Debug, PartialEq, Clone)]
3535
pub struct GitUrl {
36-
/// The input url
37-
pub href: String,
3836
/// The fully qualified domain name (FQDN) or IP of the repo
3937
pub host: Option<String>,
4038
/// The name of the repo
@@ -45,8 +43,8 @@ pub struct GitUrl {
4543
pub organization: Option<String>,
4644
/// The full name of the repo, formatted as "owner/name"
4745
pub fullname: String,
48-
/// The git url protocol
49-
pub protocol: Protocol,
46+
/// The git url scheme
47+
pub scheme: Scheme,
5048
/// The authentication user
5149
pub user: Option<String>,
5250
/// The oauth token (could appear in the https urls)
@@ -57,40 +55,89 @@ pub struct GitUrl {
5755
pub path: String,
5856
/// Indicate if url uses the .git suffix
5957
pub git_suffix: bool,
58+
/// Indicate if url explicitly uses its scheme
59+
pub scheme_prefix: bool,
6060
}
6161

62+
/// Build the printable GitUrl from its components
6263
impl fmt::Display for GitUrl {
6364
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64-
write!(f, "{}", self.href)
65+
let scheme_prefix = match self.scheme_prefix {
66+
true => format!("{}://", self.scheme),
67+
false => format!(""),
68+
};
69+
70+
let auth_info = match self.scheme {
71+
Scheme::Ssh | Scheme::Git | Scheme::GitSsh => {
72+
if let Some(user) = &self.user {
73+
format!("{}@", user)
74+
} else {
75+
format!("")
76+
}
77+
}
78+
Scheme::Http | Scheme::Https => match (&self.user, &self.token) {
79+
(Some(user), Some(token)) => format!("{}:{}@", user, token),
80+
(Some(user), None) => format!("{}@", user),
81+
(None, Some(token)) => format!("{}@", token),
82+
(None, None) => format!(""),
83+
},
84+
_ => format!(""),
85+
};
86+
87+
let host = match &self.host {
88+
Some(host) => format!("{}", host),
89+
None => format!(""),
90+
};
91+
92+
let port = match &self.port {
93+
Some(p) => format!(":{}", p),
94+
None => format!(""),
95+
};
96+
97+
let path = match &self.scheme {
98+
Scheme::Ssh => {
99+
if self.port.is_some() {
100+
format!("/{}", &self.path)
101+
} else {
102+
format!(":{}", &self.path)
103+
}
104+
}
105+
_ => format!("{}", &self.path),
106+
};
107+
108+
let git_url_str = format!("{}{}{}{}{}", scheme_prefix, auth_info, host, port, path);
109+
110+
write!(f, "{}", git_url_str)
65111
}
66112
}
67113

68114
impl Default for GitUrl {
69115
fn default() -> Self {
70116
GitUrl {
71-
href: "".to_string(),
72117
host: None,
73118
name: "".to_string(),
74119
owner: None,
75120
organization: None,
76121
fullname: "".to_string(),
77-
protocol: Protocol::Unspecified,
122+
scheme: Scheme::Unspecified,
78123
user: None,
79124
token: None,
80125
port: None,
81126
path: "".to_string(),
82-
git_suffix: true,
127+
git_suffix: false,
128+
scheme_prefix: false,
83129
}
84130
}
85131
}
86132

87133
impl GitUrl {
88-
/// Returns a new `GitUrl` with provided `url` set as `href`
89-
pub fn new(url: &str) -> GitUrl {
90-
GitUrl {
91-
href: url.to_string(),
92-
..Default::default()
93-
}
134+
/// Returns `GitUrl` after removing `user` and `token` values
135+
/// Intended use-case is for non-destructive printing GitUrl excluding any embedded auth info
136+
pub fn trim_auth(&self) -> GitUrl {
137+
let mut new_giturl = self.clone();
138+
new_giturl.user = None;
139+
new_giturl.token = None;
140+
new_giturl
94141
}
95142

96143
/// Returns a `Result<GitUrl>` after normalizing and parsing `url` for metadata
@@ -99,12 +146,12 @@ impl GitUrl {
99146
let normalized = normalize_url(url).expect("Url normalization failed");
100147

101148
// Some pre-processing for paths
102-
let protocol = Protocol::from_str(normalized.scheme())
103-
.expect(&format!("Protocol unsupported: {:?}", normalized.scheme()));
149+
let scheme = Scheme::from_str(normalized.scheme())
150+
.expect(&format!("Scheme unsupported: {:?}", normalized.scheme()));
104151

105152
// Normalized ssh urls can always have their first '/' removed
106-
let urlpath = match &protocol {
107-
Protocol::Ssh => {
153+
let urlpath = match &scheme {
154+
Scheme::Ssh => {
108155
// At the moment, we're relying on url::Url's parse() behavior to not duplicate
109156
// the leading '/' when we normalize
110157
normalized.path()[1..].to_string()
@@ -130,9 +177,9 @@ impl GitUrl {
130177

131178
let name = splitpath[0].trim_end_matches(".git").to_string();
132179

133-
let (owner, organization, fullname) = match &protocol {
180+
let (owner, organization, fullname) = match &scheme {
134181
// We're not going to assume anything about metadata from a filepath
135-
Protocol::File => (None::<String>, None::<String>, name.clone()),
182+
Scheme::File => (None::<String>, None::<String>, name.clone()),
136183
_ => {
137184
let mut fullname: Vec<&str> = Vec::new();
138185

@@ -145,11 +192,11 @@ impl GitUrl {
145192
true => {
146193
debug!("Found a git provider with an org");
147194

148-
// The path differs between git:// and https:// protocols
195+
// The path differs between git:// and https:// schemes
149196

150-
match &protocol {
197+
match &scheme {
151198
// Example: "[email protected]:v3/CompanyName/ProjectName/RepoName",
152-
Protocol::Ssh => {
199+
Scheme::Ssh => {
153200
// Organization
154201
fullname.push(splitpath[2].clone());
155202
// Project/Owner name
@@ -164,7 +211,7 @@ impl GitUrl {
164211
)
165212
}
166213
// Example: "https://[email protected]/CompanyName/ProjectName/_git/RepoName",
167-
Protocol::Https => {
214+
Scheme::Https => {
168215
// Organization
169216
fullname.push(splitpath[3].clone());
170217
// Project/Owner name
@@ -178,7 +225,7 @@ impl GitUrl {
178225
fullname.join("/").to_string(),
179226
)
180227
}
181-
_ => panic!("Protocol not supported for host"),
228+
_ => panic!("Scheme not supported for host"),
182229
}
183230
}
184231
false => {
@@ -198,7 +245,6 @@ impl GitUrl {
198245
};
199246

200247
Ok(GitUrl {
201-
href: url.to_string(),
202248
host: match normalized.host_str() {
203249
Some(h) => Some(h.to_string()),
204250
None => None,
@@ -207,7 +253,7 @@ impl GitUrl {
207253
owner: owner,
208254
organization: organization,
209255
fullname: fullname,
210-
protocol: Protocol::from_str(normalized.scheme()).expect("Protocol unsupported"),
256+
scheme: Scheme::from_str(normalized.scheme()).expect("Scheme unsupported"),
211257
user: match normalized.username().to_string().len() {
212258
0 => None,
213259
_ => Some(normalized.username().to_string()),
@@ -219,6 +265,7 @@ impl GitUrl {
219265
port: normalized.port(),
220266
path: urlpath,
221267
git_suffix: *git_suffix_check,
268+
scheme_prefix: url.contains("://"),
222269
..Default::default()
223270
})
224271
}
@@ -269,35 +316,38 @@ fn normalize_file_path(filepath: &str) -> Result<Url> {
269316
pub fn normalize_url(url: &str) -> Result<Url> {
270317
debug!("Processing: {:?}", &url);
271318

272-
let url_parse = Url::parse(&url);
319+
// We're going to remove any trailing slash before running through Url::parse
320+
let trim_url = url.trim_end_matches("/");
321+
322+
let url_parse = Url::parse(&trim_url);
273323

274324
Ok(match url_parse {
275325
Ok(u) => {
276-
match Protocol::from_str(u.scheme()) {
326+
match Scheme::from_str(u.scheme()) {
277327
Ok(_p) => u,
278328
Err(_e) => {
279329
// Catch case when an ssh url is given w/o a user
280330
debug!("Scheme parse fail. Assuming a userless ssh url");
281-
normalize_ssh_url(url)?
331+
normalize_ssh_url(trim_url)?
282332
}
283333
}
284334
}
285335
Err(_e) => {
286336
// e will most likely be url::ParseError::RelativeUrlWithoutBase
287-
// If we're here, we're only looking for Protocol::Ssh or Protocol::File
337+
// If we're here, we're only looking for Scheme::Ssh or Scheme::File
288338

289-
// Assuming we have found Protocol::Ssh if we can find an "@" before ":"
290-
// Otherwise we have Protocol::File
339+
// Assuming we have found Scheme::Ssh if we can find an "@" before ":"
340+
// Otherwise we have Scheme::File
291341
let re = Regex::new(r"^\S+(@)\S+(:).*$")?;
292342

293-
match re.is_match(&url) {
343+
match re.is_match(&trim_url) {
294344
true => {
295-
debug!("Protocol::SSH match for normalization");
296-
normalize_ssh_url(url)?
345+
debug!("Scheme::SSH match for normalization");
346+
normalize_ssh_url(trim_url)?
297347
}
298348
false => {
299-
debug!("Protocol::File match for normalization");
300-
normalize_file_path(&format!("{}", url))?
349+
debug!("Scheme::File match for normalization");
350+
normalize_file_path(&format!("{}", trim_url))?
301351
}
302352
}
303353
}

tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
mod normalize;
22
mod parse;
3+
mod trim_auth;

0 commit comments

Comments
 (0)