[O] Wording

This commit is contained in:
2026-05-10 11:07:08 +00:00
parent 7a699aee81
commit 582ea7c490
3 changed files with 128 additions and 56 deletions
+13 -7
View File
@@ -40,6 +40,12 @@ pub struct PullRequestInfo {
pub url: Option<String>, pub url: Option<String>,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebhookInstallOutcome {
Created,
Existing,
}
pub fn list_mirror_repos( pub fn list_mirror_repos(
config: &Config, config: &Config,
mirror: &MirrorConfig, mirror: &MirrorConfig,
@@ -172,7 +178,7 @@ impl<'a> ProviderClient<'a> {
repo: &RemoteRepo, repo: &RemoteRepo,
url: &str, url: &str,
secret: &str, secret: &str,
) -> Result<()> { ) -> Result<WebhookInstallOutcome> {
dispatch_provider!(self.site.provider, dispatch_provider!(self.site.provider,
github => self.github_install_webhook(endpoint, repo, url, secret), github => self.github_install_webhook(endpoint, repo, url, secret),
gitlab => self.gitlab_install_webhook(endpoint, repo, url, secret), gitlab => self.gitlab_install_webhook(endpoint, repo, url, secret),
@@ -317,7 +323,7 @@ impl<'a> ProviderClient<'a> {
repo: &RemoteRepo, repo: &RemoteRepo,
url: &str, url: &str,
secret: &str, secret: &str,
) -> Result<()> { ) -> Result<WebhookInstallOutcome> {
let hooks_url = self.repo_hooks_url(endpoint, &repo.name, "GitHub")?; let hooks_url = self.repo_hooks_url(endpoint, &repo.name, "GitHub")?;
let body = json!({ let body = json!({
"name": "web", "name": "web",
@@ -526,7 +532,7 @@ impl<'a> ProviderClient<'a> {
repo: &RemoteRepo, repo: &RemoteRepo,
url: &str, url: &str,
secret: &str, secret: &str,
) -> Result<()> { ) -> Result<WebhookInstallOutcome> {
let hooks_url = self.gitlab_hooks_url(endpoint, &repo.name); let hooks_url = self.gitlab_hooks_url(endpoint, &repo.name);
let body = json!({ let body = json!({
"url": url, "url": url,
@@ -695,7 +701,7 @@ impl<'a> ProviderClient<'a> {
repo: &RemoteRepo, repo: &RemoteRepo,
url: &str, url: &str,
secret: &str, secret: &str,
) -> Result<()> { ) -> Result<WebhookInstallOutcome> {
let hooks_url = self.repo_hooks_url(endpoint, &repo.name, "Gitea/Forgejo")?; let hooks_url = self.repo_hooks_url(endpoint, &repo.name, "Gitea/Forgejo")?;
let body = json!({ let body = json!({
"type": "gitea", "type": "gitea",
@@ -875,10 +881,10 @@ impl<'a> ProviderClient<'a> {
target_url: &str, target_url: &str,
body: &serde_json::Value, body: &serde_json::Value,
put_on_update: bool, put_on_update: bool,
) -> Result<()> { ) -> Result<WebhookInstallOutcome> {
let Some(hook) = self.find_existing_hook(hooks_url, target_url)? else { let Some(hook) = self.find_existing_hook(hooks_url, target_url)? else {
self.post_json::<serde_json::Value>(hooks_url, body)?; self.post_json::<serde_json::Value>(hooks_url, body)?;
return Ok(()); return Ok(WebhookInstallOutcome::Created);
}; };
let update_url = format!("{hooks_url}/{}", hook.id); let update_url = format!("{hooks_url}/{}", hook.id);
@@ -887,7 +893,7 @@ impl<'a> ProviderClient<'a> {
} else { } else {
self.patch_json::<serde_json::Value>(&update_url, body)?; self.patch_json::<serde_json::Value>(&update_url, body)?;
} }
Ok(()) Ok(WebhookInstallOutcome::Existing)
} }
fn delete_matching_hook(&self, hooks_url: &str, target_url: &str) -> Result<bool> { fn delete_matching_hook(&self, hooks_url: &str, target_url: &str) -> Result<bool> {
+57 -48
View File
@@ -18,7 +18,9 @@ use crate::config::{
Config, EndpointConfig, MirrorConfig, ProviderKind, RepoNameFilter, default_work_dir, Config, EndpointConfig, MirrorConfig, ProviderKind, RepoNameFilter, default_work_dir,
validate_config, validate_config,
}; };
use crate::provider::{EndpointRepo, ProviderClient, RemoteRepo, list_mirror_repos}; use crate::provider::{
EndpointRepo, ProviderClient, RemoteRepo, WebhookInstallOutcome, list_mirror_repos,
};
use crate::state::{load_toml_or_default, save_toml}; use crate::state::{load_toml_or_default, save_toml};
use crate::sync::{SyncOptions, sync_all}; use crate::sync::{SyncOptions, sync_all};
@@ -575,66 +577,73 @@ fn run_uninstall_tasks(tasks: Vec<WebhookUninstallTask>, jobs: usize) -> Result<
fn install_webhook_task(task: WebhookInstallTask, state: &Arc<Mutex<WebhookState>>) -> Result<()> { fn install_webhook_task(task: WebhookInstallTask, state: &Arc<Mutex<WebhookState>>) -> Result<()> {
let key = webhook_installation_key(&task.group, &task.endpoint, &task.repo.name); let key = webhook_installation_key(&task.group, &task.endpoint, &task.repo.name);
crate::logln!(
" {} {} {}",
style(if task.dry_run {
"would install"
} else {
"install"
})
.green()
.bold(),
style(&task.repo.name).cyan(),
style(format!("webhook on {}", task.endpoint.label())).dim()
);
if task.dry_run { if task.dry_run {
crate::logln!(
" {} {} {}",
style("would install").green().bold(),
style(&task.repo.name).cyan(),
style(format!("webhook on {}", task.endpoint.label())).dim()
);
return Ok(()); return Ok(());
} }
let client = ProviderClient::new(&task.site)?; let client = ProviderClient::new(&task.site)?;
if let Err(error) = client.install_webhook(&task.endpoint, &task.repo, &task.url, &task.secret) match client.install_webhook(&task.endpoint, &task.repo, &task.url, &task.secret) {
{ Ok(outcome) => {
if is_duplicate_webhook_error(&error) { let action = match outcome {
WebhookInstallOutcome::Created => "install",
WebhookInstallOutcome::Existing => "exists",
};
crate::logln!( crate::logln!(
" {} {} {}", " {} {} {}",
style("exists").green().bold(), style(action).green().bold(),
style(&task.repo.name).cyan(), style(&task.repo.name).cyan(),
style(format!("webhook on {}", task.endpoint.label())).dim() style(format!("webhook on {}", task.endpoint.label())).dim()
); );
record_webhook_installation(state, key, task); record_webhook_installation(state, key, task);
return Ok(()); Ok(())
} }
if let Some(reason) = non_actionable_webhook_failure_reason(&error) { Err(error) => {
crate::logln!( if is_duplicate_webhook_error(&error) {
" {} {} {}", crate::logln!(
style("skip").yellow().bold(), " {} {} {}",
style(&task.repo.name).cyan(), style("exists").green().bold(),
style(format!("webhook on {}: {reason}", task.endpoint.label())).dim() style(&task.repo.name).cyan(),
); style(format!("webhook on {}", task.endpoint.label())).dim()
let mut state = state );
.lock() record_webhook_installation(state, key, task);
.unwrap_or_else(|poisoned| poisoned.into_inner()); return Ok(());
state.skipped.insert( }
key, if let Some(reason) = non_actionable_webhook_failure_reason(&error) {
SkippedWebhookInstallation { crate::logln!(
group: task.group, " {} {} {}",
endpoint: task.endpoint, style("skip").yellow().bold(),
repo: task.repo.name, style(&task.repo.name).cyan(),
url: task.url, style(format!("webhook on {}: {reason}", task.endpoint.label())).dim()
reason, );
}, let mut state = state
); .lock()
return Ok(()); .unwrap_or_else(|poisoned| poisoned.into_inner());
state.skipped.insert(
key,
SkippedWebhookInstallation {
group: task.group,
endpoint: task.endpoint,
repo: task.repo.name,
url: task.url,
reason,
},
);
return Ok(());
}
Err(error).with_context(|| {
format!(
"failed to install webhook for {} on {}",
task.repo.name,
task.endpoint.label()
)
})
} }
return Err(error).with_context(|| {
format!(
"failed to install webhook for {} on {}",
task.repo.name,
task.endpoint.label()
)
});
} }
record_webhook_installation(state, key, task);
Ok(())
} }
fn record_webhook_installation( fn record_webhook_installation(
+58 -1
View File
@@ -309,7 +309,7 @@ fn install_webhook_posts_github_hook_when_missing() {
}; };
let client = ProviderClient::new(&site).unwrap(); let client = ProviderClient::new(&site).unwrap();
client let outcome = client
.install_webhook( .install_webhook(
&EndpointConfig { &EndpointConfig {
site: "github".to_string(), site: "github".to_string(),
@@ -326,6 +326,63 @@ fn install_webhook_posts_github_hook_when_missing() {
"secret", "secret",
) )
.unwrap(); .unwrap();
assert_eq!(outcome, WebhookInstallOutcome::Created);
handle.join().unwrap();
}
#[test]
fn install_webhook_reports_existing_forgejo_hook() {
let (api_url, handle) = request_server(
vec![
(
"200 OK",
r#"[{"id":42,"config":{"url":"https://mirror.example.test/webhook/"}}]"#,
),
("200 OK", r#"{"id":42}"#),
],
|index, request| match index {
0 => assert!(
request.starts_with("GET /repos/alice/repo/hooks "),
"request was {request}"
),
1 => {
assert!(
request.starts_with("PATCH /repos/alice/repo/hooks/42 "),
"request was {request}"
);
assert!(request.contains("https://mirror.example.test/webhook"));
assert!(request.contains("secret"));
assert!(request.contains("push"));
}
_ => unreachable!(),
},
);
let site = SiteConfig {
api_url: Some(api_url),
..site(ProviderKind::Forgejo, None)
};
let client = ProviderClient::new(&site).unwrap();
let outcome = client
.install_webhook(
&EndpointConfig {
site: "forgejo".to_string(),
kind: NamespaceKind::User,
namespace: "alice".to_string(),
},
&RemoteRepo {
name: "repo".to_string(),
clone_url: "https://codeberg.org/alice/repo.git".to_string(),
private: true,
description: None,
},
"https://mirror.example.test/webhook",
"secret",
)
.unwrap();
assert_eq!(outcome, WebhookInstallOutcome::Existing);
handle.join().unwrap(); handle.join().unwrap();
} }