[F] Webhook issues
This commit is contained in:
+29
-6
@@ -1,4 +1,4 @@
|
||||
use std::env;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -48,7 +48,6 @@ pub enum ProviderKind {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TokenConfig {
|
||||
Value(String),
|
||||
Env(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
@@ -217,7 +216,7 @@ impl Config {
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
let contents = toml::to_string_pretty(self)?;
|
||||
let mut file = fs::File::create(path)
|
||||
let mut file = create_private_file(path)
|
||||
.with_context(|| format!("failed to create {}", path.display()))?;
|
||||
file.write_all(contents.as_bytes())
|
||||
.with_context(|| format!("failed to write {}", path.display()))?;
|
||||
@@ -295,11 +294,9 @@ impl WebhookConfig {
|
||||
}
|
||||
|
||||
impl TokenConfig {
|
||||
pub fn value(&self, label: &str) -> Result<String> {
|
||||
pub fn value(&self, _label: &str) -> Result<String> {
|
||||
match self {
|
||||
TokenConfig::Value(value) => Ok(value.clone()),
|
||||
TokenConfig::Env(name) => env::var(name)
|
||||
.with_context(|| format!("environment variable {name} for {label} is not set")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,6 +323,24 @@ fn trim_end(value: &str) -> &str {
|
||||
value.trim_end_matches('/')
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn create_private_file(path: &Path) -> Result<fs::File> {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path)
|
||||
.with_context(|| format!("failed to create {}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn create_private_file(path: &Path) -> Result<fs::File> {
|
||||
fs::File::create(path).with_context(|| format!("failed to create {}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn protect_file(path: &Path) -> Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -358,7 +373,15 @@ pub fn validate_config(config: &Config) -> Result<()> {
|
||||
mirror.name
|
||||
);
|
||||
}
|
||||
let mut endpoints = BTreeSet::new();
|
||||
for endpoint in &mirror.endpoints {
|
||||
if !endpoints.insert(endpoint) {
|
||||
bail!(
|
||||
"mirror '{}' contains duplicate endpoint {}",
|
||||
mirror.name,
|
||||
endpoint.label()
|
||||
);
|
||||
}
|
||||
config.site(&endpoint.site).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"mirror '{}' references unknown site '{}'",
|
||||
|
||||
+6
-18
@@ -1,10 +1,8 @@
|
||||
use std::fmt::Display;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use console::{Term, style};
|
||||
@@ -284,7 +282,7 @@ fn prompt_webhook_setup_styled(config: &mut Config, theme: &ColorfulTheme) -> Re
|
||||
config.webhook = Some(WebhookConfig {
|
||||
install: true,
|
||||
url,
|
||||
secret: TokenConfig::Value(generate_webhook_secret()),
|
||||
secret: TokenConfig::Value(generate_webhook_secret()?),
|
||||
full_sync_interval_minutes,
|
||||
reachability_check_interval_minutes: Some(15),
|
||||
});
|
||||
@@ -1340,25 +1338,15 @@ fn validate_url(value: &str) -> std::result::Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_webhook_secret() -> String {
|
||||
fn generate_webhook_secret() -> Result<String> {
|
||||
let mut bytes = [0_u8; 32];
|
||||
if File::open("/dev/urandom")
|
||||
.and_then(|mut file| file.read_exact(&mut bytes))
|
||||
.is_err()
|
||||
{
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_nanos())
|
||||
.unwrap_or_default();
|
||||
for (index, byte) in bytes.iter_mut().enumerate() {
|
||||
*byte = ((nanos >> ((index % 16) * 8)) & 0xff) as u8;
|
||||
}
|
||||
}
|
||||
getrandom::fill(&mut bytes)
|
||||
.map_err(|error| anyhow!("failed to generate webhook secret: {error}"))?;
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
output.push_str(&format!("{byte:02x}"));
|
||||
}
|
||||
output
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+36
-6
@@ -787,9 +787,10 @@ impl<'a> ProviderClient<'a> {
|
||||
|
||||
fn find_existing_hook(&self, hooks_url: &str, target_url: &str) -> Result<Option<RepoHook>> {
|
||||
let hooks: Vec<RepoHook> = self.paged_get(hooks_url)?;
|
||||
Ok(hooks
|
||||
.into_iter()
|
||||
.find(|hook| hook.url() == Some(target_url)))
|
||||
Ok(hooks.into_iter().find(|hook| {
|
||||
hook.url()
|
||||
.is_some_and(|hook_url| webhook_urls_match(hook_url, target_url))
|
||||
}))
|
||||
}
|
||||
|
||||
fn upsert_hook(
|
||||
@@ -981,6 +982,34 @@ fn is_conflict_error(error: &anyhow::Error) -> bool {
|
||||
error.to_string().contains("409 Conflict")
|
||||
}
|
||||
|
||||
fn webhook_urls_match(left: &str, right: &str) -> bool {
|
||||
if left == right {
|
||||
return true;
|
||||
}
|
||||
match (normalize_webhook_url(left), normalize_webhook_url(right)) {
|
||||
(Some(left), Some(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_webhook_url(value: &str) -> Option<String> {
|
||||
let url = Url::parse(value).ok()?;
|
||||
let scheme = url.scheme().to_ascii_lowercase();
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return None;
|
||||
}
|
||||
let host = url.host_str()?.to_ascii_lowercase();
|
||||
let port = url.port_or_known_default()?;
|
||||
let username = url.username();
|
||||
let password = url.password().unwrap_or_default();
|
||||
let path = url.path().trim_end_matches('/');
|
||||
let path = if path.is_empty() { "/" } else { path };
|
||||
let query = url.query().unwrap_or_default();
|
||||
Some(format!(
|
||||
"{scheme}://{username}:{password}@{host}:{port}{path}?{query}"
|
||||
))
|
||||
}
|
||||
|
||||
fn next_link(headers: &HeaderMap) -> Option<String> {
|
||||
let header = headers.get("link")?.to_str().ok()?;
|
||||
for part in header.split(',') {
|
||||
@@ -1131,9 +1160,10 @@ struct PullRequestHead {
|
||||
|
||||
impl RepoHook {
|
||||
fn url(&self) -> Option<&str> {
|
||||
self.url
|
||||
.as_deref()
|
||||
.or_else(|| self.config.get("url").map(String::as_str))
|
||||
self.config
|
||||
.get("url")
|
||||
.map(String::as_str)
|
||||
.or(self.url.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-9
@@ -9,8 +9,8 @@ use console::style;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::config::{
|
||||
Config, ConflictResolutionStrategy, DEFAULT_JOBS, EndpointConfig, MirrorConfig, RepoNameFilter,
|
||||
SyncVisibility, default_work_dir, validate_config,
|
||||
Config, ConflictResolutionStrategy, DEFAULT_JOBS, EndpointConfig, MirrorConfig, NamespaceKind,
|
||||
RepoNameFilter, SyncVisibility, default_work_dir, validate_config,
|
||||
};
|
||||
use crate::git::{
|
||||
BranchConflict, BranchDeletion, BranchUpdate, GitMirror, Redactor, RemoteSpec,
|
||||
@@ -846,12 +846,8 @@ fn remote_specs(context: &RepoSyncContext<'_>, repos: &[EndpointRepo]) -> Result
|
||||
}
|
||||
let site = context.config.site(&endpoint_repo.endpoint.site).unwrap();
|
||||
let client = ProviderClient::new(site)?;
|
||||
let remote_name = safe_remote_name(&format!(
|
||||
"{}_{}",
|
||||
endpoint_repo.endpoint.site, endpoint_repo.endpoint.namespace
|
||||
));
|
||||
remotes.push(RemoteSpec {
|
||||
name: remote_name,
|
||||
name: remote_name_for_endpoint(&endpoint_repo.endpoint),
|
||||
url: client.authenticated_clone_url(&endpoint_repo.repo.clone_url)?,
|
||||
display: endpoint_repo.endpoint.label(),
|
||||
});
|
||||
@@ -1274,7 +1270,20 @@ fn remote_name_for_endpoint_repo(endpoint_repo: &EndpointRepo) -> String {
|
||||
}
|
||||
|
||||
fn remote_name_for_endpoint(endpoint: &EndpointConfig) -> String {
|
||||
safe_remote_name(&format!("{}_{}", endpoint.site, endpoint.namespace))
|
||||
format!(
|
||||
"r{}_{}_{}",
|
||||
hex_component(&endpoint.site),
|
||||
namespace_kind_key(&endpoint.kind),
|
||||
hex_component(&endpoint.namespace)
|
||||
)
|
||||
}
|
||||
|
||||
fn namespace_kind_key(kind: &NamespaceKind) -> &'static str {
|
||||
match kind {
|
||||
NamespaceKind::User => "user",
|
||||
NamespaceKind::Org => "org",
|
||||
NamespaceKind::Group => "group",
|
||||
}
|
||||
}
|
||||
|
||||
fn branch_names(branches: &[crate::git::BranchDecision]) -> BTreeSet<String> {
|
||||
@@ -1329,7 +1338,7 @@ fn hex_component(value: &str) -> String {
|
||||
}
|
||||
|
||||
fn decode_hex_component(value: &str) -> Option<String> {
|
||||
if value.len() % 2 != 0 {
|
||||
if !value.len().is_multiple_of(2) {
|
||||
return None;
|
||||
}
|
||||
let mut bytes = Vec::with_capacity(value.len() / 2);
|
||||
|
||||
+4
-28
@@ -215,7 +215,7 @@ pub fn install_webhooks(config: &Config, options: WebhookInstallOptions) -> Resu
|
||||
});
|
||||
}
|
||||
}
|
||||
run_install_tasks(tasks, options.jobs, Arc::clone(&state), false)?;
|
||||
run_install_tasks(tasks, options.jobs, Arc::clone(&state))?;
|
||||
}
|
||||
if !options.dry_run {
|
||||
let state = state
|
||||
@@ -345,7 +345,7 @@ pub fn ensure_configured_webhooks(
|
||||
dry_run: false,
|
||||
});
|
||||
}
|
||||
run_install_tasks(tasks, jobs, Arc::clone(&state), true)?;
|
||||
run_install_tasks(tasks, jobs, Arc::clone(&state))?;
|
||||
let state = state
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
@@ -480,7 +480,6 @@ fn run_install_tasks(
|
||||
tasks: Vec<WebhookInstallTask>,
|
||||
jobs: usize,
|
||||
state: Arc<Mutex<WebhookState>>,
|
||||
use_state_cache: bool,
|
||||
) -> Result<()> {
|
||||
if tasks.is_empty() {
|
||||
return Ok(());
|
||||
@@ -510,7 +509,7 @@ fn run_install_tasks(
|
||||
break;
|
||||
};
|
||||
if result_sender
|
||||
.send(install_webhook_task(task, &state, use_state_cache))
|
||||
.send(install_webhook_task(task, &state))
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
@@ -598,31 +597,8 @@ fn run_uninstall_tasks(tasks: Vec<WebhookUninstallTask>, jobs: usize) -> Result<
|
||||
Ok(removed_keys)
|
||||
}
|
||||
|
||||
fn install_webhook_task(
|
||||
task: WebhookInstallTask,
|
||||
state: &Arc<Mutex<WebhookState>>,
|
||||
use_state_cache: bool,
|
||||
) -> Result<()> {
|
||||
fn install_webhook_task(task: WebhookInstallTask, state: &Arc<Mutex<WebhookState>>) -> Result<()> {
|
||||
let key = webhook_installation_key(&task.group, &task.endpoint, &task.repo.name);
|
||||
if use_state_cache {
|
||||
let state = state
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
if state
|
||||
.installations
|
||||
.get(&key)
|
||||
.is_some_and(|installation| installation.url == task.url)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if state
|
||||
.skipped
|
||||
.get(&key)
|
||||
.is_some_and(|skipped| skipped.url == task.url)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
crate::logln!(
|
||||
" {} {} {}",
|
||||
style(if task.dry_run {
|
||||
|
||||
Reference in New Issue
Block a user