[+] Track repo deletions
This commit is contained in:
+48
-7
@@ -106,6 +106,14 @@ impl<'a> ProviderClient<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_repo(&self, endpoint: &EndpointConfig, repo_name: &str) -> Result<()> {
|
||||
dispatch_provider!(self.site.provider,
|
||||
github => self.github_delete_repo(endpoint, repo_name),
|
||||
gitlab => self.gitlab_delete_repo(endpoint, repo_name),
|
||||
gitea_like => self.gitea_delete_repo(endpoint, repo_name),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn install_webhook(
|
||||
&self,
|
||||
endpoint: &EndpointConfig,
|
||||
@@ -246,6 +254,11 @@ impl<'a> ProviderClient<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
fn github_delete_repo(&self, endpoint: &EndpointConfig, repo_name: &str) -> Result<()> {
|
||||
let url = self.repo_url(endpoint, repo_name, "GitHub")?;
|
||||
self.delete(&url).map(|_| ())
|
||||
}
|
||||
|
||||
fn github_install_webhook(
|
||||
&self,
|
||||
endpoint: &EndpointConfig,
|
||||
@@ -398,6 +411,11 @@ impl<'a> ProviderClient<'a> {
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn gitlab_delete_repo(&self, endpoint: &EndpointConfig, repo_name: &str) -> Result<()> {
|
||||
let url = self.gitlab_project_url(endpoint, repo_name);
|
||||
self.delete(&url).map(|_| ())
|
||||
}
|
||||
|
||||
fn gitlab_group(&self, namespace: &str) -> Result<GitlabGroup> {
|
||||
let url = format!("{}/groups/{}", self.site.api_base(), urlencoding(namespace));
|
||||
self.get_json(&url)
|
||||
@@ -574,6 +592,11 @@ impl<'a> ProviderClient<'a> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn gitea_delete_repo(&self, endpoint: &EndpointConfig, repo_name: &str) -> Result<()> {
|
||||
let url = self.repo_url(endpoint, repo_name, "Gitea/Forgejo")?;
|
||||
self.delete(&url).map(|_| ())
|
||||
}
|
||||
|
||||
fn gitea_install_webhook(
|
||||
&self,
|
||||
endpoint: &EndpointConfig,
|
||||
@@ -674,6 +697,22 @@ impl<'a> ProviderClient<'a> {
|
||||
Ok(closed)
|
||||
}
|
||||
|
||||
fn repo_url(
|
||||
&self,
|
||||
endpoint: &EndpointConfig,
|
||||
repo_name: &str,
|
||||
provider: &str,
|
||||
) -> Result<String> {
|
||||
if matches!(endpoint.kind, NamespaceKind::Group) {
|
||||
bail!("{provider} endpoints use kind 'user' or 'org'");
|
||||
}
|
||||
Ok(format!(
|
||||
"{}/repos/{}/{repo_name}",
|
||||
self.site.api_base(),
|
||||
endpoint.namespace
|
||||
))
|
||||
}
|
||||
|
||||
fn repo_hooks_url(
|
||||
&self,
|
||||
endpoint: &EndpointConfig,
|
||||
@@ -707,18 +746,20 @@ impl<'a> ProviderClient<'a> {
|
||||
}
|
||||
|
||||
fn gitlab_hooks_url(&self, endpoint: &EndpointConfig, repo_name: &str) -> String {
|
||||
let project = format!("{}/{repo_name}", endpoint.namespace);
|
||||
format!(
|
||||
"{}/projects/{}/hooks",
|
||||
self.site.api_base(),
|
||||
urlencoding(&project)
|
||||
)
|
||||
format!("{}/hooks", self.gitlab_project_url(endpoint, repo_name))
|
||||
}
|
||||
|
||||
fn gitlab_merge_requests_url(&self, endpoint: &EndpointConfig, repo_name: &str) -> String {
|
||||
format!(
|
||||
"{}/merge_requests",
|
||||
self.gitlab_project_url(endpoint, repo_name)
|
||||
)
|
||||
}
|
||||
|
||||
fn gitlab_project_url(&self, endpoint: &EndpointConfig, repo_name: &str) -> String {
|
||||
let project = format!("{}/{repo_name}", endpoint.namespace);
|
||||
format!(
|
||||
"{}/projects/{}/merge_requests",
|
||||
"{}/projects/{}",
|
||||
self.site.api_base(),
|
||||
urlencoding(&project)
|
||||
)
|
||||
|
||||
+237
-19
@@ -178,19 +178,24 @@ fn sync_group(
|
||||
}
|
||||
|
||||
let mut repos = repos_by_name(all_endpoint_repos);
|
||||
let all_repo_count = repos.len();
|
||||
let retry_repo_names = context
|
||||
.retry_failed_repos
|
||||
.and_then(|repos| repos.get(&mirror.name));
|
||||
let repo_names = repos
|
||||
let tracked_repo_names = context.ref_state.repo_names(&mirror.name);
|
||||
let all_repo_names = repos
|
||||
.keys()
|
||||
.cloned()
|
||||
.chain(tracked_repo_names)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let all_repo_count = all_repo_names.len();
|
||||
let repo_names = all_repo_names
|
||||
.into_iter()
|
||||
.filter(|name| {
|
||||
context
|
||||
.repo_pattern
|
||||
.is_none_or(|pattern| pattern.is_match(name))
|
||||
&& retry_repo_names.is_none_or(|repos| repos.contains(name.as_str()))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if repo_names.is_empty() {
|
||||
if let Some(retry_repo_names) = retry_repo_names {
|
||||
@@ -314,11 +319,20 @@ fn sync_group(
|
||||
for result in receiver {
|
||||
match result {
|
||||
Ok(success) => {
|
||||
if let Some(refs) = success.outcome.ref_update {
|
||||
if let Some(update) = success.outcome.state_update {
|
||||
match update {
|
||||
RepoStateUpdate::Set(refs) => {
|
||||
context
|
||||
.ref_state
|
||||
.set_repo(&mirror.name, &success.repo_name, refs);
|
||||
}
|
||||
RepoStateUpdate::Remove => {
|
||||
context
|
||||
.ref_state
|
||||
.remove_repo(&mirror.name, &success.repo_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(failure) => {
|
||||
let scope = format!("{}/{}", mirror.name, failure.repo_name);
|
||||
@@ -481,7 +495,12 @@ struct RepoSyncContext<'a> {
|
||||
|
||||
#[derive(Default)]
|
||||
struct RepoSyncOutcome {
|
||||
ref_update: Option<BTreeMap<String, RemoteRefState>>,
|
||||
state_update: Option<RepoStateUpdate>,
|
||||
}
|
||||
|
||||
enum RepoStateUpdate {
|
||||
Set(BTreeMap<String, RemoteRefState>),
|
||||
Remove,
|
||||
}
|
||||
|
||||
fn sync_repo(
|
||||
@@ -497,19 +516,31 @@ fn sync_repo(
|
||||
style("Repo").magenta().bold(),
|
||||
style(repo_name).bold()
|
||||
);
|
||||
let previous_repo_refs = ref_state.repo(&context.mirror.name, repo_name);
|
||||
if repos.is_empty() {
|
||||
crate::logln!(
|
||||
" {} {}",
|
||||
style("skip").yellow().bold(),
|
||||
style("repository not found on any endpoint").dim()
|
||||
);
|
||||
return Ok(RepoSyncOutcome::default());
|
||||
return handle_repo_deletion(
|
||||
context,
|
||||
repo_name,
|
||||
repos,
|
||||
previous_repo_refs,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.map(|outcome| outcome.unwrap_or_default());
|
||||
}
|
||||
|
||||
let initial_remotes = remote_specs(context, repos)?;
|
||||
let Some(initial_ref_state) = check_remote_refs(context, repo_name, &initial_remotes)? else {
|
||||
return Ok(RepoSyncOutcome::default());
|
||||
};
|
||||
if let Some(outcome) = handle_repo_deletion(
|
||||
context,
|
||||
repo_name,
|
||||
repos,
|
||||
previous_repo_refs,
|
||||
&initial_ref_state,
|
||||
)? {
|
||||
return Ok(outcome);
|
||||
}
|
||||
let all_endpoints_present = all_configured_endpoints_present(context.mirror, repos);
|
||||
if !context.dry_run
|
||||
&& all_endpoints_present
|
||||
@@ -539,7 +570,7 @@ fn sync_repo(
|
||||
style("up-to-date").green().bold()
|
||||
);
|
||||
return Ok(RepoSyncOutcome {
|
||||
ref_update: Some(initial_ref_state),
|
||||
state_update: Some(RepoStateUpdate::Set(initial_ref_state)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -606,8 +637,7 @@ fn sync_repo(
|
||||
&mirror_repo,
|
||||
&remotes,
|
||||
repos,
|
||||
detailed_repo_ref_state(ref_state.repo(&context.mirror.name, repo_name))
|
||||
.or(cached_ref_state.as_ref()),
|
||||
detailed_repo_ref_state(previous_repo_refs).or(cached_ref_state.as_ref()),
|
||||
&initial_ref_state,
|
||||
)?;
|
||||
if !context.dry_run && !result.had_conflicts {
|
||||
@@ -620,12 +650,117 @@ fn sync_repo(
|
||||
initial_ref_state
|
||||
};
|
||||
return Ok(RepoSyncOutcome {
|
||||
ref_update: Some(refs),
|
||||
state_update: Some(RepoStateUpdate::Set(refs)),
|
||||
});
|
||||
}
|
||||
Ok(RepoSyncOutcome::default())
|
||||
}
|
||||
|
||||
fn handle_repo_deletion(
|
||||
context: &RepoSyncContext<'_>,
|
||||
repo_name: &str,
|
||||
repos: &[EndpointRepo],
|
||||
previous_refs: Option<&BTreeMap<String, RemoteRefState>>,
|
||||
current_refs: &BTreeMap<String, RemoteRefState>,
|
||||
) -> Result<Option<RepoSyncOutcome>> {
|
||||
match repo_deletion_decision(context.mirror, repos, previous_refs, current_refs) {
|
||||
RepoDeletionDecision::None => {
|
||||
if repos.is_empty() {
|
||||
crate::logln!(
|
||||
" {} {}",
|
||||
style("skip").yellow().bold(),
|
||||
style("repository not found on any endpoint").dim()
|
||||
);
|
||||
return Ok(Some(RepoSyncOutcome::default()));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
RepoDeletionDecision::DeletedEverywhere { deleted_remotes } => {
|
||||
crate::logln!(
|
||||
" {} {} deleted on {}",
|
||||
style("deleted repo").red().bold(),
|
||||
style(repo_name).cyan(),
|
||||
deleted_remotes.join("+")
|
||||
);
|
||||
Ok(Some(RepoSyncOutcome {
|
||||
state_update: (!context.dry_run).then_some(RepoStateUpdate::Remove),
|
||||
}))
|
||||
}
|
||||
RepoDeletionDecision::Propagate {
|
||||
deleted_remotes,
|
||||
target_remotes,
|
||||
} => {
|
||||
crate::logln!(
|
||||
" {} {} deleted on {} -> {}",
|
||||
style("deleted repo").red().bold(),
|
||||
style(repo_name).cyan(),
|
||||
deleted_remotes.join("+"),
|
||||
target_remotes.join("+")
|
||||
);
|
||||
delete_repos(context, repo_name, repos, &target_remotes)?;
|
||||
Ok(Some(RepoSyncOutcome {
|
||||
state_update: (!context.dry_run).then_some(RepoStateUpdate::Remove),
|
||||
}))
|
||||
}
|
||||
RepoDeletionDecision::Conflict {
|
||||
deleted_remotes,
|
||||
changed_remotes,
|
||||
} => {
|
||||
crate::logln!(
|
||||
" {} repo {} was deleted on {} but changed on {} ({})",
|
||||
style("conflict").yellow().bold(),
|
||||
style(repo_name).cyan(),
|
||||
deleted_remotes.join("+"),
|
||||
changed_remotes.join("+"),
|
||||
style("skipped").dim()
|
||||
);
|
||||
fail_on_unresolved_conflict(context, "repo deletion conflict")?;
|
||||
Ok(Some(RepoSyncOutcome::default()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_repos(
|
||||
context: &RepoSyncContext<'_>,
|
||||
repo_name: &str,
|
||||
repos: &[EndpointRepo],
|
||||
target_remotes: &[String],
|
||||
) -> Result<()> {
|
||||
for repo in repos {
|
||||
let remote_name = remote_name_for_endpoint_repo(repo);
|
||||
if !target_remotes.contains(&remote_name) {
|
||||
continue;
|
||||
}
|
||||
crate::logln!(
|
||||
" {} {} {}",
|
||||
style(if context.dry_run {
|
||||
"would delete"
|
||||
} else {
|
||||
"delete"
|
||||
})
|
||||
.red()
|
||||
.bold(),
|
||||
style(repo_name).cyan(),
|
||||
style(format!("from {}", repo.endpoint.label())).dim()
|
||||
);
|
||||
if context.dry_run {
|
||||
continue;
|
||||
}
|
||||
let site = context.config.site(&repo.endpoint.site).unwrap();
|
||||
let client = ProviderClient::new(site)?;
|
||||
client
|
||||
.delete_repo(&repo.endpoint, repo_name)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to delete {} from {}",
|
||||
repo_name,
|
||||
repo.endpoint.label()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn all_configured_endpoints_present(mirror: &MirrorConfig, repos: &[EndpointRepo]) -> bool {
|
||||
let present = repos
|
||||
.iter()
|
||||
@@ -1145,10 +1280,11 @@ fn endpoint_repos_by_remote_name<'a>(
|
||||
}
|
||||
|
||||
fn remote_name_for_endpoint_repo(endpoint_repo: &EndpointRepo) -> String {
|
||||
safe_remote_name(&format!(
|
||||
"{}_{}",
|
||||
endpoint_repo.endpoint.site, endpoint_repo.endpoint.namespace
|
||||
))
|
||||
remote_name_for_endpoint(&endpoint_repo.endpoint)
|
||||
}
|
||||
|
||||
fn remote_name_for_endpoint(endpoint: &EndpointConfig) -> String {
|
||||
safe_remote_name(&format!("{}_{}", endpoint.site, endpoint.namespace))
|
||||
}
|
||||
|
||||
fn branch_names(branches: &[crate::git::BranchDecision]) -> BTreeSet<String> {
|
||||
@@ -1332,6 +1468,88 @@ fn branch_deletion_decisions(
|
||||
(deletions, conflicts, blocked)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum RepoDeletionDecision {
|
||||
None,
|
||||
DeletedEverywhere {
|
||||
deleted_remotes: Vec<String>,
|
||||
},
|
||||
Propagate {
|
||||
deleted_remotes: Vec<String>,
|
||||
target_remotes: Vec<String>,
|
||||
},
|
||||
Conflict {
|
||||
deleted_remotes: Vec<String>,
|
||||
changed_remotes: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn repo_deletion_decision(
|
||||
mirror: &MirrorConfig,
|
||||
repos: &[EndpointRepo],
|
||||
previous_refs: Option<&BTreeMap<String, RemoteRefState>>,
|
||||
current_refs: &BTreeMap<String, RemoteRefState>,
|
||||
) -> RepoDeletionDecision {
|
||||
let Some(previous_refs) = previous_refs else {
|
||||
return RepoDeletionDecision::None;
|
||||
};
|
||||
let remote_names = mirror
|
||||
.endpoints
|
||||
.iter()
|
||||
.map(remote_name_for_endpoint)
|
||||
.collect::<Vec<_>>();
|
||||
if remote_names.is_empty() {
|
||||
return RepoDeletionDecision::None;
|
||||
}
|
||||
|
||||
let present_remotes = repos
|
||||
.iter()
|
||||
.map(remote_name_for_endpoint_repo)
|
||||
.collect::<BTreeSet<_>>();
|
||||
if present_remotes.len() == remote_names.len() {
|
||||
return RepoDeletionDecision::None;
|
||||
}
|
||||
|
||||
let deleted_remotes = remote_names
|
||||
.iter()
|
||||
.filter(|remote| !present_remotes.contains(remote.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let target_remotes = remote_names
|
||||
.iter()
|
||||
.filter(|remote| present_remotes.contains(remote.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if target_remotes.is_empty() {
|
||||
return RepoDeletionDecision::DeletedEverywhere { deleted_remotes };
|
||||
}
|
||||
|
||||
if remote_names
|
||||
.iter()
|
||||
.any(|remote| !previous_refs.contains_key(remote))
|
||||
{
|
||||
return RepoDeletionDecision::None;
|
||||
}
|
||||
|
||||
let changed_remotes = target_remotes
|
||||
.iter()
|
||||
.filter(|remote| current_refs.get(remote.as_str()) != previous_refs.get(remote.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if changed_remotes.is_empty() {
|
||||
RepoDeletionDecision::Propagate {
|
||||
deleted_remotes,
|
||||
target_remotes,
|
||||
}
|
||||
} else {
|
||||
RepoDeletionDecision::Conflict {
|
||||
deleted_remotes,
|
||||
changed_remotes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RepoRefSyncResult {
|
||||
pushed: bool,
|
||||
had_conflicts: bool,
|
||||
|
||||
@@ -149,6 +149,23 @@ impl RefState {
|
||||
.insert(repo.to_string(), refs);
|
||||
}
|
||||
|
||||
pub(super) fn remove_repo(&mut self, group: &str, repo: &str) {
|
||||
let Some(repos) = self.repos.get_mut(group) else {
|
||||
return;
|
||||
};
|
||||
repos.remove(repo);
|
||||
if repos.is_empty() {
|
||||
self.repos.remove(group);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn repo_names(&self, group: &str) -> BTreeSet<String> {
|
||||
self.repos
|
||||
.get(group)
|
||||
.map(|repos| repos.keys().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(super) fn repo(
|
||||
&self,
|
||||
group: &str,
|
||||
|
||||
@@ -255,6 +255,105 @@ fn uninstall_webhook_deletes_matching_github_hook() {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_repo_deletes_github_repo() {
|
||||
let (api_url, handle) = one_request_server("204 No Content", "", |request| {
|
||||
assert!(
|
||||
request.starts_with("DELETE /repos/alice/repo "),
|
||||
"request was {request}"
|
||||
);
|
||||
assert!(
|
||||
request
|
||||
.to_ascii_lowercase()
|
||||
.contains("authorization: bearer secret"),
|
||||
"request was {request}"
|
||||
);
|
||||
});
|
||||
let site = SiteConfig {
|
||||
api_url: Some(api_url),
|
||||
..site(ProviderKind::Github, None)
|
||||
};
|
||||
|
||||
ProviderClient::new(&site)
|
||||
.unwrap()
|
||||
.delete_repo(
|
||||
&EndpointConfig {
|
||||
site: "github".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
"repo",
|
||||
)
|
||||
.unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_repo_deletes_url_encoded_gitlab_project() {
|
||||
let (api_url, handle) = one_request_server("202 Accepted", "", |request| {
|
||||
assert!(
|
||||
request.starts_with("DELETE /projects/parent%2Falice%2Frepo "),
|
||||
"request was {request}"
|
||||
);
|
||||
assert!(
|
||||
request
|
||||
.to_ascii_lowercase()
|
||||
.contains("private-token: secret"),
|
||||
"request was {request}"
|
||||
);
|
||||
});
|
||||
let site = SiteConfig {
|
||||
api_url: Some(api_url),
|
||||
..site(ProviderKind::Gitlab, None)
|
||||
};
|
||||
|
||||
ProviderClient::new(&site)
|
||||
.unwrap()
|
||||
.delete_repo(
|
||||
&EndpointConfig {
|
||||
site: "gitlab".to_string(),
|
||||
kind: NamespaceKind::Group,
|
||||
namespace: "parent/alice".to_string(),
|
||||
},
|
||||
"repo",
|
||||
)
|
||||
.unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_repo_deletes_gitea_repo() {
|
||||
let (api_url, handle) = one_request_server("204 No Content", "", |request| {
|
||||
assert!(
|
||||
request.starts_with("DELETE /repos/alice/repo "),
|
||||
"request was {request}"
|
||||
);
|
||||
assert!(
|
||||
request
|
||||
.to_ascii_lowercase()
|
||||
.contains("authorization: token secret"),
|
||||
"request was {request}"
|
||||
);
|
||||
});
|
||||
let site = SiteConfig {
|
||||
api_url: Some(api_url),
|
||||
..site(ProviderKind::Gitea, None)
|
||||
};
|
||||
|
||||
ProviderClient::new(&site)
|
||||
.unwrap()
|
||||
.delete_repo(
|
||||
&EndpointConfig {
|
||||
site: "gitea".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
"repo",
|
||||
)
|
||||
.unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_pull_request_posts_github_pull_when_missing() {
|
||||
let (api_url, handle) = request_server(
|
||||
|
||||
@@ -174,6 +174,140 @@ fn branch_deletion_decisions_ignore_internal_conflict_branches() {
|
||||
assert!(blocked.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_deletion_decision_propagates_previous_synced_repo_deletion() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"github_alice".to_string(),
|
||||
remote_ref_state("a", &[("main", "111")]),
|
||||
);
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
let mut current = BTreeMap::new();
|
||||
current.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
let decision = repo_deletion_decision(
|
||||
&mirror,
|
||||
&[endpoint_repo("gitea")],
|
||||
Some(&previous),
|
||||
¤t,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decision,
|
||||
RepoDeletionDecision::Propagate {
|
||||
deleted_remotes: vec!["github_alice".to_string()],
|
||||
target_remotes: vec!["gitea_alice".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_deletion_decision_conflicts_when_remaining_repo_changed() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"github_alice".to_string(),
|
||||
remote_ref_state("a", &[("main", "111")]),
|
||||
);
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
let mut current = BTreeMap::new();
|
||||
current.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_ref_state("changed", &[("main", "222")]),
|
||||
);
|
||||
|
||||
let decision = repo_deletion_decision(
|
||||
&mirror,
|
||||
&[endpoint_repo("gitea")],
|
||||
Some(&previous),
|
||||
¤t,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decision,
|
||||
RepoDeletionDecision::Conflict {
|
||||
deleted_remotes: vec!["github_alice".to_string()],
|
||||
changed_remotes: vec!["gitea_alice".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_deletion_decision_removes_state_when_deleted_everywhere() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"github_alice".to_string(),
|
||||
remote_ref_state("a", &[("main", "111")]),
|
||||
);
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
let decision = repo_deletion_decision(&mirror, &[], Some(&previous), &BTreeMap::new());
|
||||
|
||||
assert_eq!(
|
||||
decision,
|
||||
RepoDeletionDecision::DeletedEverywhere {
|
||||
deleted_remotes: vec!["github_alice".to_string(), "gitea_alice".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_deletion_decision_removes_partial_state_when_deleted_everywhere() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
let decision = repo_deletion_decision(&mirror, &[], Some(&previous), &BTreeMap::new());
|
||||
|
||||
assert_eq!(
|
||||
decision,
|
||||
RepoDeletionDecision::DeletedEverywhere {
|
||||
deleted_remotes: vec!["github_alice".to_string(), "gitea_alice".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_deletion_decision_ignores_repos_not_previously_synced_everywhere() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
let mut current = BTreeMap::new();
|
||||
current.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
let decision = repo_deletion_decision(
|
||||
&mirror,
|
||||
&[endpoint_repo("gitea")],
|
||||
Some(&previous),
|
||||
¤t,
|
||||
);
|
||||
|
||||
assert_eq!(decision, RepoDeletionDecision::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflict_branch_prefixes_are_reversible_not_slug_collisions() {
|
||||
let slash_branch = conflict_pr_branch_prefix("release/foo");
|
||||
@@ -219,3 +353,34 @@ fn test_remotes() -> Vec<RemoteSpec> {
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn test_mirror() -> MirrorConfig {
|
||||
MirrorConfig {
|
||||
name: "sync-1".to_string(),
|
||||
endpoints: vec![endpoint("github"), endpoint("gitea")],
|
||||
create_missing: true,
|
||||
visibility: crate::config::Visibility::Private,
|
||||
allow_force: false,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint(site: &str) -> EndpointConfig {
|
||||
EndpointConfig {
|
||||
site: site.to_string(),
|
||||
kind: crate::config::NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint_repo(site: &str) -> EndpointRepo {
|
||||
EndpointRepo {
|
||||
endpoint: endpoint(site),
|
||||
repo: crate::provider::RemoteRepo {
|
||||
name: "repo".to_string(),
|
||||
clone_url: format!("https://{site}.invalid/alice/repo.git"),
|
||||
private: true,
|
||||
description: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user