[O] Fast path for serve
This commit is contained in:
+15
@@ -816,6 +816,13 @@ pub fn is_disabled_repository_error(error: &anyhow::Error) -> bool {
|
|||||||
.any(|error| is_disabled_repository_stderr(error.stderr()))
|
.any(|error| is_disabled_repository_stderr(error.stderr()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_missing_repository_error(error: &anyhow::Error) -> bool {
|
||||||
|
error
|
||||||
|
.chain()
|
||||||
|
.filter_map(|cause| cause.downcast_ref::<GitCommandError>())
|
||||||
|
.any(|error| is_missing_repository_stderr(error.stderr()))
|
||||||
|
}
|
||||||
|
|
||||||
fn missing_remotes(all_remote_names: &[String], source_remotes: &[String]) -> Vec<String> {
|
fn missing_remotes(all_remote_names: &[String], source_remotes: &[String]) -> Vec<String> {
|
||||||
all_remote_names
|
all_remote_names
|
||||||
.iter()
|
.iter()
|
||||||
@@ -832,6 +839,14 @@ fn is_disabled_repository_stderr(stderr: &str) -> bool {
|
|||||||
|| stderr.contains("dmca takedown")
|
|| stderr.contains("dmca takedown")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_missing_repository_stderr(stderr: &str) -> bool {
|
||||||
|
let stderr = stderr.to_ascii_lowercase();
|
||||||
|
(stderr.contains("repository") && stderr.contains("not found"))
|
||||||
|
|| stderr.contains("project you were looking for could not be found")
|
||||||
|
|| stderr.contains("does not appear to be a git repository")
|
||||||
|
|| stderr.contains("the requested url returned error: 404")
|
||||||
|
}
|
||||||
|
|
||||||
impl Redactor {
|
impl Redactor {
|
||||||
pub fn new(secrets: Vec<String>) -> Self {
|
pub fn new(secrets: Vec<String>) -> Self {
|
||||||
let secrets = secrets
|
let secrets = secrets
|
||||||
|
|||||||
+328
-7
@@ -15,7 +15,7 @@ use crate::config::{
|
|||||||
};
|
};
|
||||||
use crate::git::{
|
use crate::git::{
|
||||||
BranchConflict, BranchDeletion, BranchUpdate, GitMirror, Redactor, RefBackup, RemoteSpec,
|
BranchConflict, BranchDeletion, BranchUpdate, GitMirror, Redactor, RefBackup, RemoteSpec,
|
||||||
is_disabled_repository_error, ls_remote_refs, safe_remote_name,
|
is_disabled_repository_error, is_missing_repository_error, ls_remote_refs, safe_remote_name,
|
||||||
};
|
};
|
||||||
use crate::logging;
|
use crate::logging;
|
||||||
use crate::provider::{
|
use crate::provider::{
|
||||||
@@ -140,6 +140,83 @@ pub fn sync_all(config: &Config, options: SyncOptions) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sync_webhook_repo(
|
||||||
|
config: &Config,
|
||||||
|
group: &str,
|
||||||
|
repo_name: &str,
|
||||||
|
work_dir: Option<PathBuf>,
|
||||||
|
jobs: usize,
|
||||||
|
) -> Result<()> {
|
||||||
|
validate_config(config)?;
|
||||||
|
if jobs == 0 {
|
||||||
|
bail!("jobs must be at least 1");
|
||||||
|
}
|
||||||
|
let work_dir = work_dir.unwrap_or_else(default_work_dir);
|
||||||
|
fs::create_dir_all(&work_dir)
|
||||||
|
.with_context(|| format!("failed to create {}", work_dir.display()))?;
|
||||||
|
let mirror = config
|
||||||
|
.mirrors
|
||||||
|
.iter()
|
||||||
|
.find(|mirror| mirror.name == group)
|
||||||
|
.with_context(|| format!("no mirror group matched '{group}'"))?;
|
||||||
|
let repo_filter = mirror.repo_filter()?;
|
||||||
|
if !repo_filter.matches(repo_name) {
|
||||||
|
crate::logln!(
|
||||||
|
" {} {} does not match configured repository filters",
|
||||||
|
style("skip").yellow().bold(),
|
||||||
|
style(repo_name).cyan()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = config
|
||||||
|
.sites
|
||||||
|
.iter()
|
||||||
|
.map(|site| site.token())
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
let redactor = Redactor::new(tokens);
|
||||||
|
let mut ref_state = load_ref_state(&work_dir)?;
|
||||||
|
crate::logln!();
|
||||||
|
crate::logln!(
|
||||||
|
"{} {}",
|
||||||
|
style("Mirror group").cyan().bold(),
|
||||||
|
style(&mirror.name).bold()
|
||||||
|
);
|
||||||
|
let mut repos = targeted_endpoint_repos(config, mirror, repo_name)?;
|
||||||
|
let context = RepoSyncContext {
|
||||||
|
config,
|
||||||
|
mirror,
|
||||||
|
work_dir: &work_dir,
|
||||||
|
redactor,
|
||||||
|
dry_run: false,
|
||||||
|
jobs,
|
||||||
|
};
|
||||||
|
let outcome = sync_assumed_repo(
|
||||||
|
&context,
|
||||||
|
repo_name,
|
||||||
|
&mut repos,
|
||||||
|
mirror.create_missing,
|
||||||
|
&ref_state,
|
||||||
|
)?;
|
||||||
|
if !outcome.created_repos.is_empty() {
|
||||||
|
webhook::ensure_configured_webhooks(
|
||||||
|
config,
|
||||||
|
mirror,
|
||||||
|
&outcome.created_repos,
|
||||||
|
&work_dir,
|
||||||
|
jobs,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if let Some(update) = outcome.state_update {
|
||||||
|
match update {
|
||||||
|
RepoStateUpdate::Set(refs) => ref_state.set_repo(&mirror.name, repo_name, refs),
|
||||||
|
RepoStateUpdate::Remove => ref_state.remove_repo(&mirror.name, repo_name),
|
||||||
|
}
|
||||||
|
save_ref_state(&work_dir, &ref_state)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
struct GroupSyncContext<'a> {
|
struct GroupSyncContext<'a> {
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
options: &'a SyncOptions,
|
options: &'a SyncOptions,
|
||||||
@@ -413,7 +490,7 @@ fn ensure_missing_repos(
|
|||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
existing: &mut Vec<EndpointRepo>,
|
existing: &mut Vec<EndpointRepo>,
|
||||||
create_missing: bool,
|
create_missing: bool,
|
||||||
) -> Result<()> {
|
) -> Result<Vec<EndpointRepo>> {
|
||||||
let present = existing
|
let present = existing
|
||||||
.iter()
|
.iter()
|
||||||
.map(|repo| repo.endpoint.clone())
|
.map(|repo| repo.endpoint.clone())
|
||||||
@@ -447,7 +524,7 @@ fn ensure_missing_repos(
|
|||||||
style(format!("on {}", endpoint.label())).dim()
|
style(format!("on {}", endpoint.label())).dim()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let description = template.and_then(|repo| repo.description);
|
let description = template.and_then(|repo| repo.description);
|
||||||
@@ -488,9 +565,13 @@ fn ensure_missing_repos(
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
created.sort_by_key(|(index, _)| *index);
|
created.sort_by_key(|(index, _)| *index);
|
||||||
existing.extend(created.into_iter().map(|(_, repo)| repo));
|
let created = created
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, repo)| repo)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
existing.extend(created.clone());
|
||||||
|
|
||||||
Ok(())
|
Ok(created)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visibility_for_created_repo(mirror: &MirrorConfig, template: Option<&RemoteRepo>) -> Visibility {
|
fn visibility_for_created_repo(mirror: &MirrorConfig, template: Option<&RemoteRepo>) -> Visibility {
|
||||||
@@ -517,6 +598,7 @@ struct RepoSyncContext<'a> {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct RepoSyncOutcome {
|
struct RepoSyncOutcome {
|
||||||
state_update: Option<RepoStateUpdate>,
|
state_update: Option<RepoStateUpdate>,
|
||||||
|
created_repos: Vec<EndpointRepo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RepoStateUpdate {
|
enum RepoStateUpdate {
|
||||||
@@ -531,6 +613,46 @@ fn mirror_repo_path(context: &RepoSyncContext<'_>, repo_name: &str) -> PathBuf {
|
|||||||
.join(format!("{}.git", safe_remote_name(repo_name)))
|
.join(format!("{}.git", safe_remote_name(repo_name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn targeted_endpoint_repos(
|
||||||
|
config: &Config,
|
||||||
|
mirror: &MirrorConfig,
|
||||||
|
repo_name: &str,
|
||||||
|
) -> Result<Vec<EndpointRepo>> {
|
||||||
|
mirror
|
||||||
|
.endpoints
|
||||||
|
.iter()
|
||||||
|
.map(|endpoint| {
|
||||||
|
let site = config.site(&endpoint.site).unwrap();
|
||||||
|
Ok(EndpointRepo {
|
||||||
|
endpoint: endpoint.clone(),
|
||||||
|
repo: RemoteRepo {
|
||||||
|
name: repo_name.to_string(),
|
||||||
|
clone_url: endpoint_clone_url(site, endpoint, repo_name)?,
|
||||||
|
private: matches!(mirror.visibility, Visibility::Private),
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_clone_url(
|
||||||
|
site: &crate::config::SiteConfig,
|
||||||
|
endpoint: &EndpointConfig,
|
||||||
|
repo_name: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
let mut url = url::Url::parse(&site.base_url)
|
||||||
|
.with_context(|| format!("invalid base URL for site '{}'", site.name))?;
|
||||||
|
let base_path = url.path().trim_end_matches('/');
|
||||||
|
let repo_path = format!("{}/{}.git", endpoint.namespace.trim_matches('/'), repo_name);
|
||||||
|
if base_path.is_empty() {
|
||||||
|
url.set_path(&repo_path);
|
||||||
|
} else {
|
||||||
|
url.set_path(&format!("{base_path}/{repo_path}"));
|
||||||
|
}
|
||||||
|
Ok(url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_repo(
|
fn sync_repo(
|
||||||
context: &RepoSyncContext<'_>,
|
context: &RepoSyncContext<'_>,
|
||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
@@ -608,7 +730,7 @@ fn sync_repo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_missing_repos(context, repo_name, repos, create_missing)?;
|
let created_repos = ensure_missing_repos(context, repo_name, repos, create_missing)?;
|
||||||
|
|
||||||
if repos.len() < 2 {
|
if repos.len() < 2 {
|
||||||
crate::logln!(
|
crate::logln!(
|
||||||
@@ -664,9 +786,143 @@ fn sync_repo(
|
|||||||
};
|
};
|
||||||
return Ok(RepoSyncOutcome {
|
return Ok(RepoSyncOutcome {
|
||||||
state_update: Some(RepoStateUpdate::Set(refs)),
|
state_update: Some(RepoStateUpdate::Set(refs)),
|
||||||
|
created_repos,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(RepoSyncOutcome::default())
|
Ok(RepoSyncOutcome {
|
||||||
|
created_repos,
|
||||||
|
..RepoSyncOutcome::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_assumed_repo(
|
||||||
|
context: &RepoSyncContext<'_>,
|
||||||
|
repo_name: &str,
|
||||||
|
repos: &mut [EndpointRepo],
|
||||||
|
create_missing: bool,
|
||||||
|
ref_state: &RefState,
|
||||||
|
) -> Result<RepoSyncOutcome> {
|
||||||
|
crate::logln!();
|
||||||
|
crate::logln!(
|
||||||
|
"{} {}",
|
||||||
|
style("Repo").magenta().bold(),
|
||||||
|
style(repo_name).bold()
|
||||||
|
);
|
||||||
|
let previous_repo_refs = ref_state.repo(&context.mirror.name, repo_name);
|
||||||
|
let all_remotes = remote_specs(context, repos)?;
|
||||||
|
let Some(initial_ref_check) = check_assumed_remote_refs(context, repo_name, &all_remotes)?
|
||||||
|
else {
|
||||||
|
return Ok(RepoSyncOutcome::default());
|
||||||
|
};
|
||||||
|
if initial_ref_check.refs.is_empty() {
|
||||||
|
crate::logln!(
|
||||||
|
" {} {}",
|
||||||
|
style("skip").yellow().bold(),
|
||||||
|
style("repository not found on any endpoint").dim()
|
||||||
|
);
|
||||||
|
return Ok(RepoSyncOutcome::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing_remote_names = initial_ref_check
|
||||||
|
.refs
|
||||||
|
.keys()
|
||||||
|
.cloned()
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let mut existing_repos = repos
|
||||||
|
.iter()
|
||||||
|
.filter(|repo| existing_remote_names.contains(&remote_name_for_endpoint_repo(repo)))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let existing_remotes = all_remotes
|
||||||
|
.iter()
|
||||||
|
.filter(|remote| existing_remote_names.contains(&remote.name))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let path = mirror_repo_path(context, repo_name);
|
||||||
|
let mirror_repo = GitMirror::open(path, context.redactor.clone(), context.dry_run)?;
|
||||||
|
mirror_repo.configure_remotes(&all_remotes)?;
|
||||||
|
let cached_ref_state = cached_ref_state(&mirror_repo, &existing_remotes)?;
|
||||||
|
backup_branches_deleted_everywhere(
|
||||||
|
context,
|
||||||
|
&mirror_repo,
|
||||||
|
repo_name,
|
||||||
|
detailed_repo_ref_state(previous_repo_refs).or(cached_ref_state.as_ref()),
|
||||||
|
&initial_ref_check.refs,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for remote in &existing_remotes {
|
||||||
|
if let Err(error) = mirror_repo.fetch_remote(remote) {
|
||||||
|
if is_disabled_repository_error(&error) {
|
||||||
|
crate::logln!(
|
||||||
|
" {} {} {}",
|
||||||
|
style("skip").yellow().bold(),
|
||||||
|
style(repo_name).cyan(),
|
||||||
|
style(format!("provider blocked access on {}", remote.display)).dim()
|
||||||
|
);
|
||||||
|
return Ok(RepoSyncOutcome::default());
|
||||||
|
}
|
||||||
|
if is_missing_repository_error(&error) {
|
||||||
|
crate::logln!(
|
||||||
|
" {} {} {}",
|
||||||
|
style("missing").yellow().bold(),
|
||||||
|
style(repo_name).cyan(),
|
||||||
|
style(format!("on {}", remote.display)).dim()
|
||||||
|
);
|
||||||
|
existing_repos.retain(|repo| remote_name_for_endpoint_repo(repo) != remote.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Err(error).with_context(|| format!("failed to fetch {}", remote.display));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let created_repos =
|
||||||
|
ensure_missing_repos(context, repo_name, &mut existing_repos, create_missing)?;
|
||||||
|
if existing_repos.len() < 2 {
|
||||||
|
crate::logln!(
|
||||||
|
" {} {} {}",
|
||||||
|
style("skip").yellow().bold(),
|
||||||
|
style(repo_name).cyan(),
|
||||||
|
style("fewer than two endpoints have this repository").dim()
|
||||||
|
);
|
||||||
|
return Ok(RepoSyncOutcome {
|
||||||
|
created_repos,
|
||||||
|
..RepoSyncOutcome::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let remotes = remote_specs(context, &existing_repos)?;
|
||||||
|
mirror_repo.configure_remotes(&remotes)?;
|
||||||
|
let result = push_repo_refs(
|
||||||
|
context,
|
||||||
|
&mirror_repo,
|
||||||
|
repo_name,
|
||||||
|
&remotes,
|
||||||
|
&existing_repos,
|
||||||
|
detailed_repo_ref_state(previous_repo_refs).or(cached_ref_state.as_ref()),
|
||||||
|
&initial_ref_check.refs,
|
||||||
|
)?;
|
||||||
|
if !context.dry_run && !result.had_conflicts {
|
||||||
|
let refs = if result.pushed {
|
||||||
|
let Some(refs) = check_remote_refs(context, repo_name, &remotes)? else {
|
||||||
|
return Ok(RepoSyncOutcome {
|
||||||
|
created_repos,
|
||||||
|
..RepoSyncOutcome::default()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
refs
|
||||||
|
} else {
|
||||||
|
initial_ref_check.refs
|
||||||
|
};
|
||||||
|
return Ok(RepoSyncOutcome {
|
||||||
|
state_update: Some(RepoStateUpdate::Set(refs)),
|
||||||
|
created_repos,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(RepoSyncOutcome {
|
||||||
|
created_repos,
|
||||||
|
..RepoSyncOutcome::default()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_repo_deletion(
|
fn handle_repo_deletion(
|
||||||
@@ -698,6 +954,7 @@ fn handle_repo_deletion(
|
|||||||
backup_deleted_repo(context, repo_name, repos, previous_refs, current_refs)?;
|
backup_deleted_repo(context, repo_name, repos, previous_refs, current_refs)?;
|
||||||
Ok(Some(RepoSyncOutcome {
|
Ok(Some(RepoSyncOutcome {
|
||||||
state_update: (!context.dry_run).then_some(RepoStateUpdate::Remove),
|
state_update: (!context.dry_run).then_some(RepoStateUpdate::Remove),
|
||||||
|
..RepoSyncOutcome::default()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
RepoDeletionDecision::Propagate {
|
RepoDeletionDecision::Propagate {
|
||||||
@@ -715,6 +972,7 @@ fn handle_repo_deletion(
|
|||||||
delete_repos(context, repo_name, repos, &target_remotes)?;
|
delete_repos(context, repo_name, repos, &target_remotes)?;
|
||||||
Ok(Some(RepoSyncOutcome {
|
Ok(Some(RepoSyncOutcome {
|
||||||
state_update: (!context.dry_run).then_some(RepoStateUpdate::Remove),
|
state_update: (!context.dry_run).then_some(RepoStateUpdate::Remove),
|
||||||
|
..RepoSyncOutcome::default()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
RepoDeletionDecision::Conflict {
|
RepoDeletionDecision::Conflict {
|
||||||
@@ -920,6 +1178,69 @@ fn check_remote_refs(
|
|||||||
Ok(Some(refs))
|
Ok(Some(refs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AssumedRemoteRefState {
|
||||||
|
refs: BTreeMap<String, RemoteRefState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_assumed_remote_refs(
|
||||||
|
context: &RepoSyncContext<'_>,
|
||||||
|
repo_name: &str,
|
||||||
|
remotes: &[RemoteSpec],
|
||||||
|
) -> Result<Option<AssumedRemoteRefState>> {
|
||||||
|
enum RemoteRefCheck {
|
||||||
|
Found(String, RemoteRefState),
|
||||||
|
Missing(String),
|
||||||
|
Blocked,
|
||||||
|
}
|
||||||
|
|
||||||
|
let ref_jobs = remotes.to_vec();
|
||||||
|
let results = crate::parallel::map(ref_jobs, context.jobs, |remote| {
|
||||||
|
crate::logln!(
|
||||||
|
" {} {}",
|
||||||
|
style("probe refs").cyan().bold(),
|
||||||
|
style(&remote.display).dim()
|
||||||
|
);
|
||||||
|
match ls_remote_refs(&remote, &context.redactor) {
|
||||||
|
Ok(snapshot) => Ok(RemoteRefCheck::Found(remote.name, snapshot.into())),
|
||||||
|
Err(error) if is_missing_repository_error(&error) => {
|
||||||
|
crate::logln!(
|
||||||
|
" {} {} {}",
|
||||||
|
style("missing").yellow().bold(),
|
||||||
|
style(repo_name).cyan(),
|
||||||
|
style(format!("on {}", remote.display)).dim()
|
||||||
|
);
|
||||||
|
Ok(RemoteRefCheck::Missing(remote.name))
|
||||||
|
}
|
||||||
|
Err(error) if is_disabled_repository_error(&error) => {
|
||||||
|
crate::logln!(
|
||||||
|
" {} {} {}",
|
||||||
|
style("skip").yellow().bold(),
|
||||||
|
style(repo_name).cyan(),
|
||||||
|
style(format!("provider blocked access on {}", remote.display)).dim()
|
||||||
|
);
|
||||||
|
Ok(RemoteRefCheck::Blocked)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
Err(error).with_context(|| format!("failed to check refs for {}", remote.display))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut refs = BTreeMap::new();
|
||||||
|
for result in results {
|
||||||
|
match result {
|
||||||
|
RemoteRefCheck::Found(remote, refs_for_remote) => {
|
||||||
|
refs.insert(remote, refs_for_remote);
|
||||||
|
}
|
||||||
|
RemoteRefCheck::Missing(remote) => {
|
||||||
|
let _ = remote;
|
||||||
|
}
|
||||||
|
RemoteRefCheck::Blocked => return Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(AssumedRemoteRefState { refs }))
|
||||||
|
}
|
||||||
|
|
||||||
fn remote_specs(context: &RepoSyncContext<'_>, repos: &[EndpointRepo]) -> Result<Vec<RemoteSpec>> {
|
fn remote_specs(context: &RepoSyncContext<'_>, repos: &[EndpointRepo]) -> Result<Vec<RemoteSpec>> {
|
||||||
let endpoint_map = context
|
let endpoint_map = context
|
||||||
.mirror
|
.mirror
|
||||||
|
|||||||
+7
-10
@@ -8,7 +8,6 @@ use std::time::Duration;
|
|||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use console::style;
|
use console::style;
|
||||||
use hmac::{Hmac, KeyInit, Mac};
|
use hmac::{Hmac, KeyInit, Mac};
|
||||||
use regex::escape;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
@@ -22,7 +21,7 @@ use crate::provider::{
|
|||||||
EndpointRepo, ProviderClient, RemoteRepo, WebhookInstallOutcome, list_mirror_repos,
|
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, sync_webhook_repo};
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
const WEBHOOK_STATE_FILE: &str = "webhook-state.toml";
|
const WEBHOOK_STATE_FILE: &str = "webhook-state.toml";
|
||||||
@@ -153,6 +152,7 @@ fn full_sync_timer_loop(
|
|||||||
&config,
|
&config,
|
||||||
SyncOptions {
|
SyncOptions {
|
||||||
work_dir: work_dir.clone(),
|
work_dir: work_dir.clone(),
|
||||||
|
jobs: config.jobs,
|
||||||
..SyncOptions::default()
|
..SyncOptions::default()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -377,15 +377,12 @@ fn worker_loop(
|
|||||||
let _sync_guard = sync_lock
|
let _sync_guard = sync_lock
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
let result = sync_all(
|
let result = sync_webhook_repo(
|
||||||
&config,
|
&config,
|
||||||
SyncOptions {
|
&job.group,
|
||||||
group: Some(job.group.clone()),
|
&job.repo,
|
||||||
repo_pattern: Some(format!("^{}$", escape(&job.repo))),
|
work_dir.clone(),
|
||||||
work_dir: work_dir.clone(),
|
config.jobs,
|
||||||
jobs: 1,
|
|
||||||
..SyncOptions::default()
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => crate::logln!(
|
Ok(()) => crate::logln!(
|
||||||
|
|||||||
@@ -41,6 +41,19 @@ fn detects_provider_disabled_repository_errors() {
|
|||||||
assert!(!is_disabled_repository_error(&generic_forbidden));
|
assert!(!is_disabled_repository_error(&generic_forbidden));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detects_missing_repository_errors() {
|
||||||
|
let error: anyhow::Error = GitCommandError::new(
|
||||||
|
"git ls-remote",
|
||||||
|
"",
|
||||||
|
"remote: Repository not found.\nfatal: repository 'https://github.com/alice/missing.git/' not found",
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
|
||||||
|
assert!(is_missing_repository_error(&error));
|
||||||
|
assert!(!is_disabled_repository_error(&error));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ls_remote_snapshot_changes_when_remote_refs_change() {
|
fn ls_remote_snapshot_changes_when_remote_refs_change() {
|
||||||
let fixture = GitFixture::new();
|
let fixture = GitFixture::new();
|
||||||
|
|||||||
@@ -448,6 +448,46 @@ fn endpoint_remote_names_do_not_slug_collide() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn targeted_endpoint_repos_synthesize_clone_urls_without_listing() {
|
||||||
|
let mirror = MirrorConfig {
|
||||||
|
name: "sync-1".to_string(),
|
||||||
|
endpoints: vec![EndpointConfig {
|
||||||
|
site: "gitlab".to_string(),
|
||||||
|
kind: crate::config::NamespaceKind::Group,
|
||||||
|
namespace: "parent/child".to_string(),
|
||||||
|
}],
|
||||||
|
sync_visibility: crate::config::SyncVisibility::All,
|
||||||
|
repo_whitelist: None,
|
||||||
|
repo_blacklist: None,
|
||||||
|
create_missing: true,
|
||||||
|
delete_missing: true,
|
||||||
|
visibility: crate::config::Visibility::Private,
|
||||||
|
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||||
|
};
|
||||||
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
|
sites: vec![crate::config::SiteConfig {
|
||||||
|
name: "gitlab".to_string(),
|
||||||
|
provider: crate::config::ProviderKind::Gitlab,
|
||||||
|
base_url: "https://gitlab.example.test/root".to_string(),
|
||||||
|
api_url: None,
|
||||||
|
token: crate::config::TokenConfig::Value("token".to_string()),
|
||||||
|
git_username: None,
|
||||||
|
}],
|
||||||
|
mirrors: vec![mirror.clone()],
|
||||||
|
webhook: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let repos = targeted_endpoint_repos(&config, &mirror, "repo").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(repos.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
repos[0].repo.clone_url,
|
||||||
|
"https://gitlab.example.test/root/parent/child/repo.git"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn created_repo_visibility_follows_existing_public_repo() {
|
fn created_repo_visibility_follows_existing_public_repo() {
|
||||||
let mirror = test_mirror();
|
let mirror = test_mirror();
|
||||||
|
|||||||
Reference in New Issue
Block a user