forked from mirrors/kingfisher
Compare commits
1 commit
blumeops
...
feature/up
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d0b2d8355 |
9 changed files with 153 additions and 3 deletions
|
|
@ -183,6 +183,10 @@ pub struct InputSpecifierArgs {
|
|||
)]
|
||||
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)]
|
||||
pub gitea_repo_type: GiteaRepoType,
|
||||
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ pub enum ScanOperation {
|
|||
pub enum ListRepositoriesCommand {
|
||||
Github { api_url: Url, specifiers: GitHubRepoSpecifiers },
|
||||
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 },
|
||||
Azure { base_url: Url, specifiers: AzureRepoSpecifiers },
|
||||
Huggingface { specifiers: HuggingFaceRepoSpecifiers },
|
||||
|
|
@ -467,6 +467,7 @@ impl ScanCommandArgs {
|
|||
if args.list_only {
|
||||
Some(ListRepositoriesCommand::Gitea {
|
||||
api_url: args.api_url,
|
||||
clone_url_base: args.clone_url_base,
|
||||
specifiers: args.specifiers,
|
||||
})
|
||||
} else {
|
||||
|
|
@ -479,6 +480,8 @@ impl ScanCommandArgs {
|
|||
args.specifiers.all_organizations;
|
||||
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_clone_url_base =
|
||||
args.clone_url_base;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -838,6 +841,15 @@ pub struct GiteaScanArgs {
|
|||
value_hint = ValueHint::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)]
|
||||
|
|
|
|||
|
|
@ -999,6 +999,7 @@ pub(crate) fn create_minimal_scan_args() -> crate::cli::commands::scan::ScanArgs
|
|||
gitea_exclude: Vec::new(),
|
||||
all_gitea_organizations: false,
|
||||
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
||||
gitea_clone_url_base: None,
|
||||
gitea_repo_type: GiteaRepoType::Source,
|
||||
bitbucket_user: 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(
|
||||
specifiers: &RepoSpecifiers,
|
||||
api_url: Url,
|
||||
clone_url_base: Option<&Url>,
|
||||
ignore_certs: bool,
|
||||
mut progress: Option<&mut ProgressBar>,
|
||||
) -> 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.dedup();
|
||||
Ok(repos)
|
||||
|
|
@ -298,6 +307,7 @@ pub async fn enumerate_repo_urls(
|
|||
|
||||
pub async fn list_repositories(
|
||||
api_url: Url,
|
||||
clone_url_base: Option<&Url>,
|
||||
ignore_certs: bool,
|
||||
progress_enabled: bool,
|
||||
users: &[String],
|
||||
|
|
@ -324,7 +334,7 @@ pub async fn list_repositories(
|
|||
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 {
|
||||
println!("{}", url);
|
||||
}
|
||||
|
|
@ -332,6 +342,15 @@ pub async fn list_repositories(
|
|||
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)> {
|
||||
let url = Url::parse(repo_url.as_str()).ok()?;
|
||||
let host = url.host_str()?.to_string();
|
||||
|
|
@ -371,4 +390,28 @@ mod tests {
|
|||
fn normalize_repo_identifier_handles_git_suffix() {
|
||||
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?;
|
||||
}
|
||||
ListRepositoriesCommand::Gitea { api_url, specifiers } => {
|
||||
ListRepositoriesCommand::Gitea { api_url, clone_url_base, specifiers } => {
|
||||
gitea::list_repositories(
|
||||
api_url,
|
||||
clone_url_base.as_ref(),
|
||||
global_args.ignore_certs,
|
||||
global_args.use_progress(),
|
||||
&specifiers.user,
|
||||
|
|
@ -1636,6 +1637,7 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs {
|
|||
gitea_exclude: Vec::new(),
|
||||
all_gitea_organizations: false,
|
||||
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
||||
gitea_clone_url_base: None,
|
||||
gitea_repo_type: GiteaRepoType::Source,
|
||||
|
||||
bitbucket_user: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -1781,6 +1781,7 @@ mod tests {
|
|||
gitea_exclude: Vec::new(),
|
||||
all_gitea_organizations: false,
|
||||
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
||||
gitea_clone_url_base: None,
|
||||
gitea_repo_type: GiteaRepoType::Source,
|
||||
bitbucket_user: Vec::new(),
|
||||
bitbucket_workspace: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ mod tests {
|
|||
gitea_exclude: Vec::new(),
|
||||
all_gitea_organizations: false,
|
||||
gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
|
||||
gitea_clone_url_base: None,
|
||||
gitea_repo_type: GiteaRepoType::Source,
|
||||
|
||||
// Bitbucket
|
||||
|
|
|
|||
|
|
@ -496,9 +496,11 @@ pub async fn enumerate_gitea_repos(
|
|||
|
||||
let mut num_found: u64 = 0;
|
||||
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(
|
||||
&repo_specifiers,
|
||||
api_url,
|
||||
clone_url_base,
|
||||
global_args.ignore_certs,
|
||||
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