[O] Explicit delete missing question, local backups
This commit is contained in:
@@ -25,6 +25,7 @@ fn parses_value_tokens() {
|
||||
repo_whitelist = "^important-|-mirror$"
|
||||
repo_blacklist = "-archive$"
|
||||
create_missing = true
|
||||
delete_missing = false
|
||||
visibility = "private"
|
||||
conflict_resolution = "auto_rebase_pull_request"
|
||||
|
||||
@@ -57,6 +58,7 @@ fn parses_value_tokens() {
|
||||
config.mirrors[0].repo_blacklist,
|
||||
Some("-archive$".to_string())
|
||||
);
|
||||
assert!(!config.mirrors[0].delete_missing);
|
||||
let webhook = config.webhook.unwrap();
|
||||
assert!(webhook.install);
|
||||
assert_eq!(webhook.url, "https://mirror.example.test/webhook");
|
||||
@@ -92,6 +94,30 @@ fn config_defaults_jobs() {
|
||||
assert_eq!(config.jobs, DEFAULT_JOBS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mirror_defaults_to_deleting_missing_repos_for_existing_configs() {
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
[[mirrors]]
|
||||
name = "personal"
|
||||
create_missing = true
|
||||
|
||||
[[mirrors.endpoints]]
|
||||
site = "github"
|
||||
kind = "user"
|
||||
namespace = "alice"
|
||||
|
||||
[[mirrors.endpoints]]
|
||||
site = "gitea"
|
||||
kind = "user"
|
||||
namespace = "alice"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(config.mirrors[0].delete_missing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_unknown_sites_and_single_endpoint_groups() {
|
||||
let config = Config {
|
||||
@@ -108,6 +134,7 @@ fn validation_rejects_unknown_sites_and_single_endpoint_groups() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -137,6 +164,7 @@ fn validation_rejects_unknown_sites_and_single_endpoint_groups() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -241,6 +269,7 @@ fn validation_rejects_duplicate_mirror_endpoints() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -287,6 +316,7 @@ fn mirror_config() -> MirrorConfig {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}
|
||||
|
||||
@@ -275,6 +275,44 @@ fn delete_branches_removes_branch_from_target_remotes() {
|
||||
assert!(!fixture.remote_ref_exists(&fixture.remote_b, "refs/heads/main"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backup_refs_create_restorable_bundle_before_branch_delete() {
|
||||
let fixture = GitFixture::new();
|
||||
let expected = fixture.commit("base", "base", 1_700_000_000);
|
||||
fixture.push_head(&fixture.remote_a, "main");
|
||||
fixture.push_head(&fixture.remote_b, "main");
|
||||
|
||||
let mirror = fixture.mirror();
|
||||
fixture.fetch_all(&mirror);
|
||||
let backup_ref = "refs/refray-backups/branches/main/test/a".to_string();
|
||||
mirror
|
||||
.backup_refs(&[RefBackup {
|
||||
refname: backup_ref.clone(),
|
||||
sha: expected.clone(),
|
||||
description: "branch main before delete".to_string(),
|
||||
}])
|
||||
.unwrap();
|
||||
let bundle = fixture._temp.path().join("branch-backup.bundle");
|
||||
mirror
|
||||
.create_bundle(&bundle, std::slice::from_ref(&backup_ref))
|
||||
.unwrap();
|
||||
|
||||
mirror
|
||||
.delete_branches(
|
||||
&fixture.remotes(),
|
||||
&[BranchDeletion {
|
||||
branch: "main".to_string(),
|
||||
deleted_remotes: vec!["a".to_string()],
|
||||
target_remotes: vec!["b".to_string()],
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let heads = git_output(None, ["bundle", "list-heads", bundle.to_str().unwrap()]);
|
||||
assert!(heads.contains(&expected));
|
||||
assert!(heads.contains(&backup_ref));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_decisions_mirror_matching_or_missing_tags_and_skip_divergent_tags() {
|
||||
let fixture = GitFixture::new();
|
||||
|
||||
@@ -14,6 +14,8 @@ fn wizard_builds_sync_group_from_profile_urls() {
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"n",
|
||||
"4",
|
||||
]
|
||||
@@ -46,6 +48,7 @@ fn wizard_builds_sync_group_from_profile_urls() {
|
||||
assert_eq!(config.mirrors[0].endpoints[1].namespace, "azalea");
|
||||
assert_eq!(config.mirrors[0].sync_visibility, SyncVisibility::All);
|
||||
assert!(config.mirrors[0].create_missing);
|
||||
assert!(config.mirrors[0].delete_missing);
|
||||
assert_eq!(config.mirrors[0].visibility, Visibility::Private);
|
||||
assert_eq!(
|
||||
config.mirrors[0].conflict_resolution,
|
||||
@@ -54,6 +57,9 @@ fn wizard_builds_sync_group_from_profile_urls() {
|
||||
|
||||
let output = String::from_utf8(output).unwrap();
|
||||
assert!(output.contains("1. github.com/hykilpikonna <-> gitea.example.test/azalea"));
|
||||
assert!(output.contains("Deletion backups: refray keeps a local backup"));
|
||||
assert!(output.contains("Create repositories that are missing from an endpoint?"));
|
||||
assert!(output.contains("delete it everywhere?"));
|
||||
assert!(output.contains("Add another sync group"));
|
||||
assert!(output.contains("Edit an existing group"));
|
||||
assert!(output.contains("Delete an existing group"));
|
||||
@@ -77,6 +83,8 @@ fn wizard_can_build_three_way_sync() {
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"n",
|
||||
"4",
|
||||
]
|
||||
@@ -92,6 +100,35 @@ fn wizard_can_build_three_way_sync() {
|
||||
assert_eq!(config.sites.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_can_disable_missing_repo_creation_and_repo_delete_propagation() {
|
||||
let input = [
|
||||
"https://github.com/alice",
|
||||
"gh-token",
|
||||
"",
|
||||
"https://gitea.example.test/alice",
|
||||
"gt-token",
|
||||
"",
|
||||
"n",
|
||||
"",
|
||||
"",
|
||||
"n",
|
||||
"n",
|
||||
"",
|
||||
"n",
|
||||
"4",
|
||||
]
|
||||
.join("\n")
|
||||
+ "\n";
|
||||
let mut reader = Cursor::new(input.as_bytes());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let config = run_config_wizard_with_io(Config::default(), &mut reader, &mut output).unwrap();
|
||||
|
||||
assert!(!config.mirrors[0].create_missing);
|
||||
assert!(!config.mirrors[0].delete_missing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_can_enable_webhooks() {
|
||||
let input = [
|
||||
@@ -105,6 +142,8 @@ fn wizard_can_enable_webhooks() {
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"y",
|
||||
"https://mirror.example.test/webhook",
|
||||
"y",
|
||||
@@ -159,6 +198,8 @@ fn wizard_reuses_existing_credentials_for_same_instance() {
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"n",
|
||||
"4",
|
||||
]
|
||||
@@ -214,6 +255,7 @@ fn wizard_starts_existing_config_at_sync_group_menu() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -243,6 +285,7 @@ fn wizard_can_ask_to_run_full_sync_after_config() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -319,6 +362,7 @@ fn wizard_edits_existing_sync_group_from_menu() {
|
||||
repo_whitelist: Some("^important-".to_string()),
|
||||
repo_blacklist: Some("-archive$".to_string()),
|
||||
create_missing: false,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Public,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -337,6 +381,8 @@ fn wizard_edits_existing_sync_group_from_menu() {
|
||||
"^public-",
|
||||
"-skip$",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"n",
|
||||
"4",
|
||||
]
|
||||
@@ -356,6 +402,7 @@ fn wizard_edits_existing_sync_group_from_menu() {
|
||||
assert_eq!(mirror.endpoints[1].site, "gitlab");
|
||||
assert_eq!(mirror.endpoints[1].namespace, "bob");
|
||||
assert!(!mirror.create_missing);
|
||||
assert!(mirror.delete_missing);
|
||||
assert_eq!(mirror.sync_visibility, SyncVisibility::Public);
|
||||
assert_eq!(mirror.repo_whitelist, Some("^public-".to_string()));
|
||||
assert_eq!(mirror.repo_blacklist, Some("-skip$".to_string()));
|
||||
@@ -406,12 +453,13 @@ fn wizard_prefills_existing_sync_group_when_editing() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
webhook: None,
|
||||
};
|
||||
let input = ["2", "1", "", "", "", "", "n", "", "", "", "n", "4"].join("\n") + "\n";
|
||||
let input = ["2", "1", "", "", "", "", "n", "", "", "", "", "", "n", "4"].join("\n") + "\n";
|
||||
let mut reader = Cursor::new(input.as_bytes());
|
||||
let mut output = Vec::new();
|
||||
|
||||
@@ -470,6 +518,7 @@ fn wizard_deletes_existing_sync_group_from_menu() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -529,6 +578,7 @@ fn wizard_can_go_back_from_delete_menu() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
|
||||
@@ -65,6 +65,9 @@ where
|
||||
let endpoints = prompt_sync_group_endpoints(reader, writer, config, &[])?;
|
||||
let sync_visibility = prompt_sync_visibility(reader, writer, None)?;
|
||||
let repo_filters = prompt_repo_filters(reader, writer, None)?;
|
||||
write_deletion_backup_notice(writer)?;
|
||||
let create_missing = prompt_create_missing(reader, writer, None)?;
|
||||
let delete_missing = prompt_delete_missing(reader, writer, None)?;
|
||||
let conflict_resolution = prompt_conflict_resolution(reader, writer, None)?;
|
||||
config.upsert_mirror(MirrorConfig {
|
||||
name: next_mirror_name(config),
|
||||
@@ -72,7 +75,8 @@ where
|
||||
sync_visibility,
|
||||
repo_whitelist: repo_filters.whitelist,
|
||||
repo_blacklist: repo_filters.blacklist,
|
||||
create_missing: true,
|
||||
create_missing,
|
||||
delete_missing,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution,
|
||||
});
|
||||
@@ -276,6 +280,8 @@ where
|
||||
whitelist: config.mirrors[index - 1].repo_whitelist.clone(),
|
||||
blacklist: config.mirrors[index - 1].repo_blacklist.clone(),
|
||||
};
|
||||
let existing_create_missing = config.mirrors[index - 1].create_missing;
|
||||
let existing_delete_missing = config.mirrors[index - 1].delete_missing;
|
||||
let existing_conflict_resolution =
|
||||
config.mirrors[index - 1].conflict_resolution.clone();
|
||||
let endpoints = prompt_sync_group_endpoints(reader, writer, config, &existing)?;
|
||||
@@ -283,6 +289,11 @@ where
|
||||
prompt_sync_visibility(reader, writer, Some(&existing_sync_visibility))?;
|
||||
let repo_filters =
|
||||
prompt_repo_filters(reader, writer, Some(&existing_repo_filters))?;
|
||||
write_deletion_backup_notice(writer)?;
|
||||
let create_missing =
|
||||
prompt_create_missing(reader, writer, Some(existing_create_missing))?;
|
||||
let delete_missing =
|
||||
prompt_delete_missing(reader, writer, Some(existing_delete_missing))?;
|
||||
let conflict_resolution = prompt_conflict_resolution(
|
||||
reader,
|
||||
writer,
|
||||
@@ -292,6 +303,8 @@ where
|
||||
config.mirrors[index - 1].sync_visibility = sync_visibility;
|
||||
config.mirrors[index - 1].repo_whitelist = repo_filters.whitelist;
|
||||
config.mirrors[index - 1].repo_blacklist = repo_filters.blacklist;
|
||||
config.mirrors[index - 1].create_missing = create_missing;
|
||||
config.mirrors[index - 1].delete_missing = delete_missing;
|
||||
config.mirrors[index - 1].conflict_resolution = conflict_resolution;
|
||||
prompt_webhook_setup(reader, writer, config)?;
|
||||
writeln!(writer, "updated sync group {index}")?;
|
||||
@@ -572,6 +585,51 @@ where
|
||||
Ok(parse_repo_pattern(&value))
|
||||
}
|
||||
|
||||
fn write_deletion_backup_notice<W>(writer: &mut W) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
writeln!(
|
||||
writer,
|
||||
"Deletion backups: refray keeps a local backup before propagating repository or branch deletes."
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_create_missing<R, W>(
|
||||
reader: &mut R,
|
||||
writer: &mut W,
|
||||
existing: Option<bool>,
|
||||
) -> Result<bool>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
prompt_bool(
|
||||
reader,
|
||||
writer,
|
||||
"Create repositories that are missing from an endpoint?",
|
||||
existing.unwrap_or(true),
|
||||
)
|
||||
}
|
||||
|
||||
fn prompt_delete_missing<R, W>(
|
||||
reader: &mut R,
|
||||
writer: &mut W,
|
||||
existing: Option<bool>,
|
||||
) -> Result<bool>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
prompt_bool(
|
||||
reader,
|
||||
writer,
|
||||
"When a previously synced repository is deleted from one endpoint, delete it everywhere?",
|
||||
existing.unwrap_or(true),
|
||||
)
|
||||
}
|
||||
|
||||
fn sync_visibility_value(sync_visibility: &SyncVisibility) -> &'static str {
|
||||
match sync_visibility {
|
||||
SyncVisibility::All => "all",
|
||||
|
||||
@@ -174,6 +174,29 @@ fn branch_deletion_decisions_ignore_internal_conflict_branches() {
|
||||
assert!(blocked.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branches_deleted_everywhere_are_backed_up_before_prune() {
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"github".to_string(),
|
||||
remote_ref_state("a", &[("main", "111")]),
|
||||
);
|
||||
previous.insert(
|
||||
"gitea".to_string(),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
let backups = branches_deleted_everywhere_backups(&previous, &BTreeMap::new(), "stamp");
|
||||
|
||||
assert_eq!(backups.len(), 1);
|
||||
assert_eq!(backups[0].sha, "111");
|
||||
assert!(
|
||||
backups[0]
|
||||
.refname
|
||||
.starts_with("refs/refray-backups/branches/")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_deletion_decision_propagates_previous_synced_repo_deletion() {
|
||||
let mirror = test_mirror();
|
||||
@@ -208,6 +231,35 @@ fn repo_deletion_decision_propagates_previous_synced_repo_deletion() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_deletion_decision_is_disabled_by_mirror_policy() {
|
||||
let mut mirror = test_mirror();
|
||||
mirror.delete_missing = false;
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
remote_key("github"),
|
||||
remote_ref_state("a", &[("main", "111")]),
|
||||
);
|
||||
previous.insert(
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
let mut current = BTreeMap::new();
|
||||
current.insert(
|
||||
remote_key("gitea"),
|
||||
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 repo_deletion_decision_conflicts_when_remaining_repo_changed() {
|
||||
let mirror = test_mirror();
|
||||
@@ -475,6 +527,7 @@ fn test_mirror() -> MirrorConfig {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: crate::config::Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ fn matches_jobs_by_provider_and_namespace() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -142,6 +143,7 @@ fn matching_jobs_respects_repo_name_filters() {
|
||||
repo_whitelist: Some("^important-".to_string()),
|
||||
repo_blacklist: Some("-archive$".to_string()),
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
};
|
||||
@@ -360,6 +362,7 @@ fn uninstall_webhooks_skips_blocked_provider_access() {
|
||||
repo_whitelist: None,
|
||||
repo_blacklist: None,
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
@@ -713,6 +716,7 @@ fn filtered_mirror() -> MirrorConfig {
|
||||
repo_whitelist: Some("^important-".to_string()),
|
||||
repo_blacklist: Some("-archive$".to_string()),
|
||||
create_missing: true,
|
||||
delete_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user