[F] Fix gitea pagination
This commit is contained in:
+47
-9
@@ -544,16 +544,21 @@ impl<'a> ProviderClient<'a> {
|
|||||||
fn gitea_list_repos(&self, endpoint: &EndpointConfig) -> Result<Vec<RemoteRepo>> {
|
fn gitea_list_repos(&self, endpoint: &EndpointConfig) -> Result<Vec<RemoteRepo>> {
|
||||||
match endpoint.kind {
|
match endpoint.kind {
|
||||||
NamespaceKind::User => {
|
NamespaceKind::User => {
|
||||||
let url = format!("{}/user/repos?limit=50", self.site.api_base());
|
let url = format!("{}/user/repos", self.site.api_base());
|
||||||
owned_repos!(self, GiteaRepo, &url, &endpoint.namespace)
|
Ok(self
|
||||||
|
.gitea_paged_get::<GiteaRepo>(&url)?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|repo| repo.owner.login.eq_ignore_ascii_case(&endpoint.namespace))
|
||||||
|
.map(Into::into)
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
NamespaceKind::Org => {
|
NamespaceKind::Org => {
|
||||||
let url = format!(
|
let url = format!("{}/orgs/{}/repos", self.site.api_base(), endpoint.namespace);
|
||||||
"{}/orgs/{}/repos?limit=50",
|
Ok(self
|
||||||
self.site.api_base(),
|
.gitea_paged_get::<GiteaRepo>(&url)?
|
||||||
endpoint.namespace
|
.into_iter()
|
||||||
);
|
.map(Into::into)
|
||||||
self.paged_remote_repos::<GiteaRepo>(&url)
|
.collect())
|
||||||
}
|
}
|
||||||
NamespaceKind::Group => bail!("Gitea/Forgejo endpoints use kind 'user' or 'org'"),
|
NamespaceKind::Group => bail!("Gitea/Forgejo endpoints use kind 'user' or 'org'"),
|
||||||
}
|
}
|
||||||
@@ -579,7 +584,14 @@ impl<'a> ProviderClient<'a> {
|
|||||||
"description": description.unwrap_or(""),
|
"description": description.unwrap_or(""),
|
||||||
"auto_init": false,
|
"auto_init": false,
|
||||||
});
|
});
|
||||||
self.post_json::<GiteaRepo>(&url, &body).map(Into::into)
|
match self.post_json::<GiteaRepo>(&url, &body) {
|
||||||
|
Ok(repo) => Ok(repo.into()),
|
||||||
|
Err(error) if is_conflict_error(&error) => {
|
||||||
|
let repo_url = self.repo_url(endpoint, name, "Gitea/Forgejo")?;
|
||||||
|
self.get_json::<GiteaRepo>(&repo_url).map(Into::into)
|
||||||
|
}
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gitea_detect_namespace_kind(&self, namespace: &str) -> Result<Option<NamespaceKind>> {
|
fn gitea_detect_namespace_kind(&self, namespace: &str) -> Result<Option<NamespaceKind>> {
|
||||||
@@ -840,6 +852,28 @@ impl<'a> ProviderClient<'a> {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gitea_paged_get<T>(&self, base_url: &str) -> Result<Vec<T>>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
const LIMIT: usize = 50;
|
||||||
|
let mut output = Vec::new();
|
||||||
|
for page in 1.. {
|
||||||
|
let separator = if base_url.contains('?') { '&' } else { '?' };
|
||||||
|
let url = format!("{base_url}{separator}page={page}&limit={LIMIT}");
|
||||||
|
let mut items: Vec<T> = self
|
||||||
|
.get(&url)?
|
||||||
|
.json()
|
||||||
|
.with_context(|| format!("invalid JSON from {url}"))?;
|
||||||
|
let count = items.len();
|
||||||
|
output.append(&mut items);
|
||||||
|
if count < LIMIT {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_json<T>(&self, url: &str) -> Result<T>
|
fn get_json<T>(&self, url: &str) -> Result<T>
|
||||||
where
|
where
|
||||||
T: for<'de> Deserialize<'de>,
|
T: for<'de> Deserialize<'de>,
|
||||||
@@ -943,6 +977,10 @@ fn is_not_found_error(error: &anyhow::Error) -> bool {
|
|||||||
error.to_string().contains("404 Not Found")
|
error.to_string().contains("404 Not Found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_conflict_error(error: &anyhow::Error) -> bool {
|
||||||
|
error.to_string().contains("409 Conflict")
|
||||||
|
}
|
||||||
|
|
||||||
fn next_link(headers: &HeaderMap) -> Option<String> {
|
fn next_link(headers: &HeaderMap) -> Option<String> {
|
||||||
let header = headers.get("link")?.to_str().ok()?;
|
let header = headers.get("link")?.to_str().ok()?;
|
||||||
for part in header.split(',') {
|
for part in header.split(',') {
|
||||||
|
|||||||
+31
-1
@@ -1079,10 +1079,25 @@ impl ProviderAccount {
|
|||||||
username: String,
|
username: String,
|
||||||
token: String,
|
token: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let mut base_url = trim_url(&base_url).to_string();
|
||||||
|
let mut username = username;
|
||||||
|
if let Ok(url) = Url::parse(&username) {
|
||||||
|
if let Some(host) = url.host_str() {
|
||||||
|
let path = url.path().trim_matches('/');
|
||||||
|
if !path.is_empty() {
|
||||||
|
let mut profile_base_url = format!("{}://{}", url.scheme(), host);
|
||||||
|
if let Some(port) = url.port() {
|
||||||
|
profile_base_url.push_str(&format!(":{port}"));
|
||||||
|
}
|
||||||
|
base_url = profile_base_url;
|
||||||
|
username = path.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
site_name: site_name.into(),
|
site_name: site_name.into(),
|
||||||
kind,
|
kind,
|
||||||
base_url: trim_url(&base_url).to_string(),
|
base_url,
|
||||||
username,
|
username,
|
||||||
token,
|
token,
|
||||||
http: Client::builder()
|
http: Client::builder()
|
||||||
@@ -1503,6 +1518,21 @@ impl ProviderAccount {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_account_derives_base_url_from_profile_url_username() {
|
||||||
|
let account = ProviderAccount::new(
|
||||||
|
"gitea",
|
||||||
|
ProviderKind::Gitea,
|
||||||
|
"https://gitea.com".to_string(),
|
||||||
|
"https://gitea.aza.moe/refray-test".to_string(),
|
||||||
|
"secret".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(account.base_url, "https://gitea.aza.moe");
|
||||||
|
assert_eq!(account.username, "refray-test");
|
||||||
|
assert_eq!(account.api_base(), "https://gitea.aza.moe/api/v1");
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum ProviderKind {
|
enum ProviderKind {
|
||||||
Github,
|
Github,
|
||||||
|
|||||||
@@ -354,6 +354,109 @@ fn delete_repo_deletes_gitea_repo() {
|
|||||||
handle.join().unwrap();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_gitea_repos_uses_page_and_limit_pagination() {
|
||||||
|
let mut first_page = Vec::new();
|
||||||
|
for index in 0..50 {
|
||||||
|
first_page.push(format!(
|
||||||
|
r#"{{"name":"repo-{index}","clone_url":"https://gitea.example.test/alice/repo-{index}.git","private":false,"description":null,"owner":{{"login":"alice"}}}}"#
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let first_page = format!("[{}]", first_page.join(","));
|
||||||
|
let second_page = r#"[
|
||||||
|
{"name":"repo-50","clone_url":"https://gitea.example.test/alice/repo-50.git","private":false,"description":null,"owner":{"login":"Alice"}},
|
||||||
|
{"name":"other-owner","clone_url":"https://gitea.example.test/bob/other-owner.git","private":false,"description":null,"owner":{"login":"bob"}}
|
||||||
|
]"#
|
||||||
|
.to_string();
|
||||||
|
let (api_url, handle) = request_server_owned(
|
||||||
|
vec![("200 OK", first_page), ("200 OK", second_page)],
|
||||||
|
|index, request| match index {
|
||||||
|
0 => assert!(
|
||||||
|
request.starts_with("GET /user/repos?page=1&limit=50 "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
1 => assert!(
|
||||||
|
request.starts_with("GET /user/repos?page=2&limit=50 "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let site = SiteConfig {
|
||||||
|
api_url: Some(api_url),
|
||||||
|
..site(ProviderKind::Gitea, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let repos = ProviderClient::new(&site)
|
||||||
|
.unwrap()
|
||||||
|
.list_repos(&EndpointConfig {
|
||||||
|
site: "gitea".to_string(),
|
||||||
|
kind: NamespaceKind::User,
|
||||||
|
namespace: "alice".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(repos.len(), 51);
|
||||||
|
assert!(repos.iter().any(|repo| repo.name == "repo-50"));
|
||||||
|
assert!(!repos.iter().any(|repo| repo.name == "other-owner"));
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_gitea_repo_returns_existing_repo_on_conflict() {
|
||||||
|
let (api_url, handle) = request_server(
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
"409 Conflict",
|
||||||
|
r#"{"message":"The repository with the same name already exists."}"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"200 OK",
|
||||||
|
r#"{"name":"repo","clone_url":"https://gitea.example.test/alice/repo.git","private":false,"description":"existing","owner":{"login":"alice"}}"#,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|index, request| match index {
|
||||||
|
0 => {
|
||||||
|
assert!(
|
||||||
|
request.starts_with("POST /user/repos "),
|
||||||
|
"request was {request}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
request.contains(r#""name":"repo""#),
|
||||||
|
"request was {request}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
1 => assert!(
|
||||||
|
request.starts_with("GET /repos/alice/repo "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let site = SiteConfig {
|
||||||
|
api_url: Some(api_url),
|
||||||
|
..site(ProviderKind::Gitea, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let repo = ProviderClient::new(&site)
|
||||||
|
.unwrap()
|
||||||
|
.create_repo(
|
||||||
|
&EndpointConfig {
|
||||||
|
site: "gitea".to_string(),
|
||||||
|
kind: NamespaceKind::User,
|
||||||
|
namespace: "alice".to_string(),
|
||||||
|
},
|
||||||
|
"repo",
|
||||||
|
&Visibility::Public,
|
||||||
|
Some("description"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(repo.name, "repo");
|
||||||
|
assert_eq!(repo.clone_url, "https://gitea.example.test/alice/repo.git");
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_pull_request_posts_github_pull_when_missing() {
|
fn open_pull_request_posts_github_pull_when_missing() {
|
||||||
let (api_url, handle) = request_server(
|
let (api_url, handle) = request_server(
|
||||||
@@ -508,6 +611,22 @@ where
|
|||||||
|
|
||||||
fn request_server<F>(
|
fn request_server<F>(
|
||||||
responses: Vec<(&'static str, &'static str)>,
|
responses: Vec<(&'static str, &'static str)>,
|
||||||
|
assert_request: F,
|
||||||
|
) -> (String, thread::JoinHandle<()>)
|
||||||
|
where
|
||||||
|
F: FnMut(usize, &str) + Send + 'static,
|
||||||
|
{
|
||||||
|
request_server_owned(
|
||||||
|
responses
|
||||||
|
.into_iter()
|
||||||
|
.map(|(status, body)| (status, body.to_string()))
|
||||||
|
.collect(),
|
||||||
|
assert_request,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_server_owned<F>(
|
||||||
|
responses: Vec<(&'static str, String)>,
|
||||||
mut assert_request: F,
|
mut assert_request: F,
|
||||||
) -> (String, thread::JoinHandle<()>)
|
) -> (String, thread::JoinHandle<()>)
|
||||||
where
|
where
|
||||||
|
|||||||
Reference in New Issue
Block a user