forked from mirrors/kingfisher
Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
162aeb5a67 | ||
| 5d0b2d8355 |
9 changed files with 153 additions and 3 deletions
|
|
@ -183,6 +183,10 @@ pub struct InputSpecifierArgs {
|
||||||
)]
|
)]
|
||||||
pub gitea_api_url: Url,
|
pub gitea_api_url: Url,
|
||||||
|
|
||||||
|
/// Override base URL for cloning Gitea repositories
|
||||||
|
#[arg(long, value_hint = ValueHint::Url, hide = true)]
|
||||||
|
pub gitea_clone_url_base: Option<Url>,
|
||||||
|
|
||||||
#[arg(long, default_value_t = GiteaRepoType::Source, hide = true)]
|
#[arg(long, default_value_t = GiteaRepoType::Source, hide = true)]
|
||||||
pub gitea_repo_type: GiteaRepoType,
|
pub gitea_repo_type: GiteaRepoType,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,7 @@ pub enum ScanOperation {
|
||||||
pub enum ListRepositoriesCommand {
|
pub enum ListRepositoriesCommand {
|
||||||
Github { api_url: Url, specifiers: GitHubRepoSpecifiers },
|
Github { api_url: Url, specifiers: GitHubRepoSpecifiers },
|
||||||
Gitlab { api_url: Url, specifiers: GitLabRepoSpecifiers },
|
Gitlab { api_url: Url, specifiers: GitLabRepoSpecifiers },
|
||||||
Gitea { api_url: Url, specifiers: GiteaRepoSpecifiers },
|
Gitea { api_url: Url, clone_url_base: Option<Url>, specifiers: GiteaRepoSpecifiers },
|
||||||
Bitbucket { api_url: Url, specifiers: BitbucketRepoSpecifiers },
|
Bitbucket { api_url: Url, specifiers: BitbucketRepoSpecifiers },
|
||||||
Azure { base_url: Url, specifiers: AzureRepoSpecifiers },
|
Azure { base_url: Url, specifiers: AzureRepoSpecifiers },
|
||||||
Huggingface { specifiers: HuggingFaceRepoSpecifiers },
|
Huggingface { specifiers: HuggingFaceRepoSpecifiers },
|
||||||
|
|
@ -467,6 +467,7 @@ impl ScanCommandArgs {
|
||||||
if args.list_only {
|
if args.list_only {
|
||||||
Some(ListRepositoriesCommand::Gitea {
|
Some(ListRepositoriesCommand::Gitea {
|
||||||
api_url: args.api_url,
|
api_url: args.api_url,
|
||||||
|
clone_url_base: args.clone_url_base,
|
||||||
specifiers: args.specifiers,
|
specifiers: args.specifiers,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -479,6 +480,8 @@ impl ScanCommandArgs {
|
||||||
args.specifiers.all_organizations;
|
args.specifiers.all_organizations;
|
||||||
scan_args.input_specifier_args.gitea_repo_type = args.specifiers.repo_type;
|
scan_args.input_specifier_args.gitea_repo_type = args.specifiers.repo_type;
|
||||||
scan_args.input_specifier_args.gitea_api_url = args.api_url;
|
scan_args.input_specifier_args.gitea_api_url = args.api_url;
|
||||||
|
scan_args.input_specifier_args.gitea_clone_url_base =
|
||||||
|
args.clone_url_base;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -838,6 +841,15 @@ pub struct GiteaScanArgs {
|
||||||
value_hint = ValueHint::Url
|
value_hint = ValueHint::Url
|
||||||
)]
|
)]
|
||||||
pub api_url: Url,
|
pub api_url: Url,
|
||||||
|
|
||||||
|
/// Override the base URL used for cloning repositories.
|
||||||
|
///
|
||||||
|
/// By default, clone URLs returned by the Gitea/Forgejo API are used as-is.
|
||||||
|
/// When the API is reachable at a different hostname than the git clone
|
||||||
|
/// endpoint (e.g., internal API vs. public clone URL), use this flag to
|
||||||
|
/// rewrite the scheme, host, and port of clone URLs.
|
||||||
|
#[arg(long = "clone-url-base", value_hint = ValueHint::Url)]
|
||||||
|
pub clone_url_base: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug, Clone)]
|
#[derive(Args, Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -999,6 +999,7 @@ pub(crate) fn create_minimal_scan_args() -> crate::cli::commands::scan::ScanArgs
|
||||||
gitea_exclude: Vec::new(),
|
gitea_exclude: Vec::new(),
|
||||||
all_gitea_organizations: false,
|
all_gitea_organizations: false,
|
||||||
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
||||||
|
gitea_clone_url_base: None,
|
||||||
gitea_repo_type: GiteaRepoType::Source,
|
gitea_repo_type: GiteaRepoType::Source,
|
||||||
bitbucket_user: Vec::new(),
|
bitbucket_user: Vec::new(),
|
||||||
bitbucket_workspace: Vec::new(),
|
bitbucket_workspace: Vec::new(),
|
||||||
|
|
|
||||||
45
src/gitea.rs
45
src/gitea.rs
|
|
@ -212,6 +212,7 @@ async fn fetch_authenticated_orgs(
|
||||||
pub async fn enumerate_repo_urls(
|
pub async fn enumerate_repo_urls(
|
||||||
specifiers: &RepoSpecifiers,
|
specifiers: &RepoSpecifiers,
|
||||||
api_url: Url,
|
api_url: Url,
|
||||||
|
clone_url_base: Option<&Url>,
|
||||||
ignore_certs: bool,
|
ignore_certs: bool,
|
||||||
mut progress: Option<&mut ProgressBar>,
|
mut progress: Option<&mut ProgressBar>,
|
||||||
) -> Result<Vec<String>> {
|
) -> Result<Vec<String>> {
|
||||||
|
|
@ -291,6 +292,14 @@ pub async fn enumerate_repo_urls(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rewrite clone URLs if a custom base was provided.
|
||||||
|
if let Some(base) = clone_url_base {
|
||||||
|
repos = repos
|
||||||
|
.into_iter()
|
||||||
|
.map(|raw| rewrite_clone_url(&raw, base).unwrap_or(raw))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
repos.sort();
|
repos.sort();
|
||||||
repos.dedup();
|
repos.dedup();
|
||||||
Ok(repos)
|
Ok(repos)
|
||||||
|
|
@ -298,6 +307,7 @@ pub async fn enumerate_repo_urls(
|
||||||
|
|
||||||
pub async fn list_repositories(
|
pub async fn list_repositories(
|
||||||
api_url: Url,
|
api_url: Url,
|
||||||
|
clone_url_base: Option<&Url>,
|
||||||
ignore_certs: bool,
|
ignore_certs: bool,
|
||||||
progress_enabled: bool,
|
progress_enabled: bool,
|
||||||
users: &[String],
|
users: &[String],
|
||||||
|
|
@ -324,7 +334,7 @@ pub async fn list_repositories(
|
||||||
exclude_repos: exclude_repos.to_vec(),
|
exclude_repos: exclude_repos.to_vec(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let urls = enumerate_repo_urls(&specifiers, api_url, ignore_certs, Some(&mut progress)).await?;
|
let urls = enumerate_repo_urls(&specifiers, api_url, clone_url_base, ignore_certs, Some(&mut progress)).await?;
|
||||||
for url in urls {
|
for url in urls {
|
||||||
println!("{}", url);
|
println!("{}", url);
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +342,15 @@ pub async fn list_repositories(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rewrite a clone URL to use a different base (scheme, host, port), preserving the path.
|
||||||
|
fn rewrite_clone_url(raw: &str, base: &Url) -> Option<String> {
|
||||||
|
let mut parsed = Url::parse(raw).ok()?;
|
||||||
|
parsed.set_scheme(base.scheme()).ok()?;
|
||||||
|
parsed.set_host(base.host_str()).ok()?;
|
||||||
|
parsed.set_port(base.port()).ok()?;
|
||||||
|
Some(parsed.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_repo(repo_url: &GitUrl) -> Option<(String, String, String)> {
|
fn parse_repo(repo_url: &GitUrl) -> Option<(String, String, String)> {
|
||||||
let url = Url::parse(repo_url.as_str()).ok()?;
|
let url = Url::parse(repo_url.as_str()).ok()?;
|
||||||
let host = url.host_str()?.to_string();
|
let host = url.host_str()?.to_string();
|
||||||
|
|
@ -371,4 +390,28 @@ mod tests {
|
||||||
fn normalize_repo_identifier_handles_git_suffix() {
|
fn normalize_repo_identifier_handles_git_suffix() {
|
||||||
assert_eq!(normalize_repo_identifier("owner/repo.git"), Some("owner/repo".into()));
|
assert_eq!(normalize_repo_identifier("owner/repo.git"), Some("owner/repo".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_clone_url_changes_host() {
|
||||||
|
let base = Url::parse("https://forge.internal.example.com/").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
rewrite_clone_url("https://forge.public.example.com/owner/repo.git", &base),
|
||||||
|
Some("https://forge.internal.example.com/owner/repo.git".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_clone_url_changes_port() {
|
||||||
|
let base = Url::parse("https://forge.example.com:3000/").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
rewrite_clone_url("https://forge.example.com/owner/repo.git", &base),
|
||||||
|
Some("https://forge.example.com:3000/owner/repo.git".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_clone_url_returns_none_for_invalid_url() {
|
||||||
|
let base = Url::parse("https://forge.example.com/").unwrap();
|
||||||
|
assert_eq!(rewrite_clone_url("not-a-url", &base), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1495,9 +1495,10 @@ async fn async_main(args: CommandLineArgs, matches: clap::ArgMatches) -> Result<
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
ListRepositoriesCommand::Gitea { api_url, specifiers } => {
|
ListRepositoriesCommand::Gitea { api_url, clone_url_base, specifiers } => {
|
||||||
gitea::list_repositories(
|
gitea::list_repositories(
|
||||||
api_url,
|
api_url,
|
||||||
|
clone_url_base.as_ref(),
|
||||||
global_args.ignore_certs,
|
global_args.ignore_certs,
|
||||||
global_args.use_progress(),
|
global_args.use_progress(),
|
||||||
&specifiers.user,
|
&specifiers.user,
|
||||||
|
|
@ -1636,6 +1637,7 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs {
|
||||||
gitea_exclude: Vec::new(),
|
gitea_exclude: Vec::new(),
|
||||||
all_gitea_organizations: false,
|
all_gitea_organizations: false,
|
||||||
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
||||||
|
gitea_clone_url_base: None,
|
||||||
gitea_repo_type: GiteaRepoType::Source,
|
gitea_repo_type: GiteaRepoType::Source,
|
||||||
|
|
||||||
bitbucket_user: Vec::new(),
|
bitbucket_user: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -1781,6 +1781,7 @@ mod tests {
|
||||||
gitea_exclude: Vec::new(),
|
gitea_exclude: Vec::new(),
|
||||||
all_gitea_organizations: false,
|
all_gitea_organizations: false,
|
||||||
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
||||||
|
gitea_clone_url_base: None,
|
||||||
gitea_repo_type: GiteaRepoType::Source,
|
gitea_repo_type: GiteaRepoType::Source,
|
||||||
bitbucket_user: Vec::new(),
|
bitbucket_user: Vec::new(),
|
||||||
bitbucket_workspace: Vec::new(),
|
bitbucket_workspace: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ mod tests {
|
||||||
gitea_exclude: Vec::new(),
|
gitea_exclude: Vec::new(),
|
||||||
all_gitea_organizations: false,
|
all_gitea_organizations: false,
|
||||||
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
||||||
|
gitea_clone_url_base: None,
|
||||||
gitea_repo_type: GiteaRepoType::Source,
|
gitea_repo_type: GiteaRepoType::Source,
|
||||||
|
|
||||||
// Bitbucket
|
// Bitbucket
|
||||||
|
|
|
||||||
|
|
@ -496,9 +496,11 @@ pub async fn enumerate_gitea_repos(
|
||||||
|
|
||||||
let mut num_found: u64 = 0;
|
let mut num_found: u64 = 0;
|
||||||
let api_url = args.input_specifier_args.gitea_api_url.clone();
|
let api_url = args.input_specifier_args.gitea_api_url.clone();
|
||||||
|
let clone_url_base = args.input_specifier_args.gitea_clone_url_base.as_ref();
|
||||||
let repo_strings = gitea::enumerate_repo_urls(
|
let repo_strings = gitea::enumerate_repo_urls(
|
||||||
&repo_specifiers,
|
&repo_specifiers,
|
||||||
api_url,
|
api_url,
|
||||||
|
clone_url_base,
|
||||||
global_args.ignore_certs,
|
global_args.ignore_certs,
|
||||||
Some(&mut progress),
|
Some(&mut progress),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
84
tests/int_gitea_clone_url_base.rs
Normal file
84
tests/int_gitea_clone_url_base.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
// tests/int_gitea_clone_url_base.rs
|
||||||
|
//
|
||||||
|
// Integration test: verify that --clone-url-base rewrites clone URLs
|
||||||
|
// returned by the Gitea API during repository enumeration.
|
||||||
|
//
|
||||||
|
// Uses wiremock to mock the Gitea API and assert_cmd to exercise the full
|
||||||
|
// CLI path: argument parsing → API enumeration → URL rewriting → output.
|
||||||
|
|
||||||
|
use assert_cmd::Command;
|
||||||
|
use predicates::str::contains;
|
||||||
|
use wiremock::{
|
||||||
|
matchers::{method, path, query_param},
|
||||||
|
Mock, MockServer, ResponseTemplate,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Run `kingfisher scan gitea --list-only` against a mock Gitea API with and
|
||||||
|
/// without --clone-url-base, verifying that clone URLs are rewritten.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn clone_url_base_rewrites_listed_urls() {
|
||||||
|
let mock_server = MockServer::start().await;
|
||||||
|
|
||||||
|
let public_host = "https://forge.public.example.com";
|
||||||
|
let repo_json = serde_json::json!([{
|
||||||
|
"full_name": "eblume/kingfisher",
|
||||||
|
"clone_url": format!("{public_host}/eblume/kingfisher.git"),
|
||||||
|
"fork": false
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Page 1: return one repo.
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/api/v1/users/eblume/repos"))
|
||||||
|
.and(query_param("page", "1"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(&repo_json))
|
||||||
|
.mount(&mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Page 2: return empty array to terminate pagination.
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/api/v1/users/eblume/repos"))
|
||||||
|
.and(query_param("page", "2"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
|
||||||
|
.mount(&mock_server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let api_url = format!("{}/api/v1/", mock_server.uri());
|
||||||
|
|
||||||
|
// WITH --clone-url-base: URLs should be rewritten.
|
||||||
|
Command::new(assert_cmd::cargo::cargo_bin!("kingfisher"))
|
||||||
|
.args([
|
||||||
|
"scan",
|
||||||
|
"gitea",
|
||||||
|
"--api-url",
|
||||||
|
&api_url,
|
||||||
|
"--clone-url-base",
|
||||||
|
"https://forge.internal.example.com/",
|
||||||
|
"--user",
|
||||||
|
"eblume",
|
||||||
|
"--list-only",
|
||||||
|
"--no-update-check",
|
||||||
|
"--quiet",
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("https://forge.internal.example.com/eblume/kingfisher.git"));
|
||||||
|
|
||||||
|
// WITHOUT --clone-url-base: URLs should be unchanged.
|
||||||
|
Command::new(assert_cmd::cargo::cargo_bin!("kingfisher"))
|
||||||
|
.args([
|
||||||
|
"scan",
|
||||||
|
"gitea",
|
||||||
|
"--api-url",
|
||||||
|
&api_url,
|
||||||
|
"--user",
|
||||||
|
"eblume",
|
||||||
|
"--list-only",
|
||||||
|
"--no-update-check",
|
||||||
|
"--quiet",
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains(&format!(
|
||||||
|
"{public_host}/eblume/kingfisher.git"
|
||||||
|
)));
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue