[F] Fix gitlab project listing

This commit is contained in:
2026-05-10 01:07:47 +00:00
parent b0469d80a7
commit 915a63a955
2 changed files with 176 additions and 3 deletions
+77 -2
View File
@@ -371,7 +371,19 @@ impl<'a> ProviderClient<'a> {
self.site.api_base(),
endpoint.namespace
);
self.paged_remote_repos::<GitlabProject>(&url)
let mut projects = self.paged_get::<GitlabProject>(&url)?;
let owned_url = format!(
"{}/projects?owned=true&simple=true&per_page=100",
self.site.api_base()
);
for project in self.paged_get::<GitlabProject>(&owned_url)? {
if project.is_in_namespace(&endpoint.namespace)
&& !projects.iter().any(|existing| existing.same_path(&project))
{
projects.push(project);
}
}
Ok(projects.into_iter().map(Into::into).collect())
}
NamespaceKind::Org | NamespaceKind::Group => {
let encoded = urlencoding(&endpoint.namespace);
@@ -411,8 +423,20 @@ impl<'a> ProviderClient<'a> {
}
let url = format!("{}/projects", self.site.api_base());
self.post_json::<GitlabProject>(&url, &serde_json::Value::Object(body))
match self.post_json::<GitlabProject>(&url, &serde_json::Value::Object(body)) {
Ok(project) => Ok(project.into()),
Err(error) if is_already_taken_error(&error) => {
let existing_url = self.gitlab_project_url(endpoint, name);
self.get_json::<GitlabProject>(&existing_url)
.map(Into::into)
.with_context(|| {
format!(
"GitLab reported that {name} already exists, but failed to fetch {existing_url}: {error:#}"
)
})
}
Err(error) => Err(error),
}
}
fn gitlab_delete_repo(&self, endpoint: &EndpointConfig, repo_name: &str) -> Result<()> {
@@ -982,6 +1006,16 @@ fn is_conflict_error(error: &anyhow::Error) -> bool {
error.to_string().contains("409 Conflict")
}
fn is_already_taken_error(error: &anyhow::Error) -> bool {
let text = error
.chain()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
.to_ascii_lowercase();
text.contains("has already been taken")
}
fn webhook_urls_match(left: &str, right: &str) -> bool {
if left == right {
return true;
@@ -1066,11 +1100,52 @@ impl From<GithubRepo> for RemoteRepo {
struct GitlabProject {
name: String,
path: Option<String>,
path_with_namespace: Option<String>,
namespace: Option<GitlabNamespace>,
http_url_to_repo: String,
visibility: String,
description: Option<String>,
}
impl GitlabProject {
fn project_path(&self) -> &str {
self.path.as_deref().unwrap_or(&self.name)
}
fn is_in_namespace(&self, namespace: &str) -> bool {
self.namespace
.as_ref()
.and_then(GitlabNamespace::full_path)
.is_some_and(|path| path.eq_ignore_ascii_case(namespace))
|| self
.path_with_namespace
.as_deref()
.and_then(|path| path.rsplit_once('/').map(|(namespace, _)| namespace))
.is_some_and(|path| path.eq_ignore_ascii_case(namespace))
}
fn same_path(&self, other: &Self) -> bool {
match (&self.path_with_namespace, &other.path_with_namespace) {
(Some(left), Some(right)) => left.eq_ignore_ascii_case(right),
_ => self
.project_path()
.eq_ignore_ascii_case(other.project_path()),
}
}
}
#[derive(Deserialize)]
struct GitlabNamespace {
path: Option<String>,
full_path: Option<String>,
}
impl GitlabNamespace {
fn full_path(&self) -> Option<&str> {
self.full_path.as_deref().or(self.path.as_deref())
}
}
impl From<GitlabProject> for RemoteRepo {
fn from(value: GitlabProject) -> Self {
Self {
+98
View File
@@ -184,6 +184,104 @@ fn detect_namespace_kind_uses_authenticated_gitea_api() {
handle.join().unwrap();
}
#[test]
fn list_gitlab_user_repos_merges_authenticated_owned_projects() {
let owned_projects = r#"[
{"name":"repo","path":"repo","path_with_namespace":"alice/repo","http_url_to_repo":"https://gitlab.example.test/alice/repo.git","visibility":"private","description":null,"namespace":{"path":"alice","full_path":"alice"}},
{"name":"other","path":"other","path_with_namespace":"bob/other","http_url_to_repo":"https://gitlab.example.test/bob/other.git","visibility":"public","description":null,"namespace":{"path":"bob","full_path":"bob"}}
]"#;
let (api_url, handle) = request_server(
vec![("200 OK", "[]"), ("200 OK", owned_projects)],
|index, request| match index {
0 => assert!(
request
.starts_with("GET /users/alice/projects?simple=true&per_page=100&owned=true "),
"request was {request}"
),
1 => assert!(
request.starts_with("GET /projects?owned=true&simple=true&per_page=100 "),
"request was {request}"
),
_ => unreachable!(),
},
);
let site = SiteConfig {
api_url: Some(api_url),
..site(ProviderKind::Gitlab, None)
};
let repos = ProviderClient::new(&site)
.unwrap()
.list_repos(&EndpointConfig {
site: "gitlab".to_string(),
kind: NamespaceKind::User,
namespace: "alice".to_string(),
})
.unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].name, "repo");
assert!(repos[0].private);
handle.join().unwrap();
}
#[test]
fn create_gitlab_repo_returns_existing_repo_when_path_is_taken() {
let existing = r#"{"name":"repo","path":"repo","path_with_namespace":"alice/repo","http_url_to_repo":"https://gitlab.example.test/alice/repo.git","visibility":"public","description":"existing","namespace":{"path":"alice","full_path":"alice"}}"#;
let (api_url, handle) = request_server(
vec![
(
"400 Bad Request",
r#"{"message":{"name":["has already been taken"],"path":["has already been taken"]}}"#,
),
("200 OK", existing),
],
|index, request| match index {
0 => {
assert!(
request.starts_with("POST /projects "),
"request was {request}"
);
assert!(
request.contains(r#""name":"repo""#),
"request was {request}"
);
assert!(
request.contains(r#""path":"repo""#),
"request was {request}"
);
}
1 => assert!(
request.starts_with("GET /projects/alice%2Frepo "),
"request was {request}"
),
_ => unreachable!(),
},
);
let site = SiteConfig {
api_url: Some(api_url),
..site(ProviderKind::Gitlab, None)
};
let repo = ProviderClient::new(&site)
.unwrap()
.create_repo(
&EndpointConfig {
site: "gitlab".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://gitlab.example.test/alice/repo.git");
handle.join().unwrap();
}
#[test]
fn install_webhook_posts_github_hook_when_missing() {
let (api_url, handle) = request_server(