Skip to content

Commit 3220dfe

Browse files
committed
cli: parse github repository url (close #307)
1 parent 71cd092 commit 3220dfe

File tree

5 files changed

+92
-4
lines changed

5 files changed

+92
-4
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10+
## [0.9.0] - 2025-08-14
11+
12+
### Added
13+
14+
- You can now also pass GitHub URL as repository argument to every
15+
subcommand ([#307](https://github.com/devmatteini/dra/issues/307))
16+
17+
```shell
18+
dra download https://github.com/devmatteini/dra-tests
19+
```
20+
1021
## [0.8.2] - 2025-05-28
1122

1223
- Fix windows executable by including static crt ([#302](https://github.com/devmatteini/dra/issues/302))

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ xz2 = "0.1.7"
3434
bzip2 = "0.6.0"
3535
urlencoding = "2.1.3"
3636
itertools = "0.14.0"
37+
url = "2.5.4"
3738

3839
[dev-dependencies]
3940
test-case = "3.3.1"

src/cli/root_command.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub struct Cli {
3939
pub enum Command {
4040
/// Select and download an asset
4141
Download {
42-
/// GitHub repository using format {owner}/{repo}
42+
/// GitHub repository using format {owner}/{repo} or the repository URL https://github.com/{owner}/{repo}
4343
#[arg(value_parser = Repository::try_parse)]
4444
repo: Repository,
4545

@@ -95,7 +95,7 @@ pub enum Command {
9595

9696
/// Select an asset and generate an untagged version of it
9797
Untag {
98-
/// GitHub repository using format {owner}/{repo}
98+
/// GitHub repository using format {owner}/{repo} or the repository URL https://github.com/{owner}/{repo}
9999
#[arg(value_parser = Repository::try_parse)]
100100
repo: Repository,
101101
},

src/github/repository.rs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::fmt::Formatter;
2+
use url::Url;
23

34
#[derive(Debug, Eq, PartialEq, Clone)]
45
pub struct Repository {
@@ -11,10 +12,19 @@ impl Repository {
1112
if src.is_empty() {
1213
return Err("Invalid repository. Cannot be empty".to_string());
1314
}
14-
if !src.contains('/') {
15+
16+
if src.starts_with("http://github.com") || src.starts_with("https://github.com") {
17+
Self::parse_url(src)
18+
} else {
19+
Self::parse(src)
20+
}
21+
}
22+
23+
fn parse(input: &str) -> Result<Repository, String> {
24+
if !input.contains('/') {
1525
return Err("Invalid repository. Use {owner}/{repo} format".to_string());
1626
}
17-
let parts = src
27+
let parts = input
1828
.split('/')
1929
.filter(|x| !x.is_empty())
2030
.collect::<Vec<&str>>();
@@ -27,6 +37,23 @@ impl Repository {
2737
repo: parts[1].to_string(),
2838
})
2939
}
40+
41+
fn parse_url(input: &str) -> Result<Repository, String> {
42+
let github_url = Url::parse(input).map_err(|x| format!("Invalid repository URL: {}", x))?;
43+
let parts = github_url
44+
.path()
45+
.split('/')
46+
.filter(|x| !x.is_empty())
47+
.collect::<Vec<&str>>();
48+
if parts.len() < 2 {
49+
return Err("Invalid repository URL. Missing owner or repo".to_string());
50+
}
51+
52+
Ok(Repository {
53+
owner: parts[0].to_string(),
54+
repo: parts[1].to_string(),
55+
})
56+
}
3057
}
3158

3259
impl std::fmt::Display for Repository {
@@ -54,6 +81,36 @@ mod tests {
5481
);
5582
}
5683

84+
#[test]
85+
fn valid_repository_from_url() {
86+
let input = "https://github.com/foo/bar?tab=readme-ov-file";
87+
88+
let result = Repository::try_parse(input);
89+
90+
assert_eq!(
91+
Ok(Repository {
92+
owner: "foo".to_string(),
93+
repo: "bar".to_string()
94+
}),
95+
result
96+
);
97+
}
98+
99+
#[test]
100+
fn valid_repository_url_from_any_page() {
101+
let input = "https://github.com/foo/bar/actions/runs/16966254957";
102+
103+
let result = Repository::try_parse(input);
104+
105+
assert_eq!(
106+
Ok(Repository {
107+
owner: "foo".to_string(),
108+
repo: "bar".to_string()
109+
}),
110+
result
111+
);
112+
}
113+
57114
#[test]
58115
fn missing_owner() {
59116
let input = "/bar";
@@ -72,6 +129,24 @@ mod tests {
72129
assert_error(|e| assert_contains("Missing owner or repo", e), result);
73130
}
74131

132+
#[test]
133+
fn missing_owner_in_url() {
134+
let input = "https://github.com";
135+
136+
let result = Repository::try_parse(input);
137+
138+
assert_error(|e| assert_contains("Missing owner or repo", e), result);
139+
}
140+
141+
#[test]
142+
fn missing_repo_in_url() {
143+
let input = "https://github.com/foo/";
144+
145+
let result = Repository::try_parse(input);
146+
147+
assert_error(|e| assert_contains("Missing owner or repo", e), result);
148+
}
149+
75150
#[test]
76151
fn empty_repository() {
77152
let input = "";

0 commit comments

Comments
 (0)