[O] Default allow changing branch rules

This commit is contained in:
2026-05-12 01:58:19 +00:00
parent ae2bd9aaa1
commit ecd19528e3
12 changed files with 541 additions and 6 deletions
+28 -2
View File
@@ -1,11 +1,12 @@
name: Build executables
"on":
release:
types:
- published
push:
branches:
- main
tags:
- "v*"
pull_request:
workflow_dispatch:
@@ -148,3 +149,28 @@ jobs:
name: ${{ env.BIN_NAME }}-linux-${{ matrix.arch }}-musl
path: ${{ env.BIN_NAME }}-linux-${{ matrix.arch }}-musl.tar.gz
if-no-files-found: error
release-assets:
name: Attach release assets
runs-on: ubuntu-latest
needs:
- windows-x86_64
- macos-universal
- linux-musl
if: github.event_name == 'release'
permissions:
contents: write
steps:
- name: Download packaged binaries
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- name: Attach binaries to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.release.tag_name }}
files: release-assets/*
fail_on_unmatched_files: true
overwrite_files: true
+8 -1
View File
@@ -12,7 +12,7 @@ Created becasue github is so unusable and [unreliable](https://red-squares.cian.
- **read-write mirrors**: Make changes from any provider, and the changes will sync to the others
- **webhook support**: Sync right after push, reduce potential divergence window
- **conflict handling**: Rebase or open pull requests when two platforms diverge
- **tracks deletions**: Branches/repo deletions sync across platforms (with backup)
- **tracks state**: Branches/repo deletions and force pushes also sync across platforms (with backup)
- **selective sync**: Sync subset of repos by regex white/black list, or by private/public visibility
- **multithreaded**: Process multiple repos simultaneously!
@@ -210,6 +210,11 @@ refray webhook update https://new.example.com/webhook
Issues and pull requests are not mirrored.
## TODO
- [ ] Sync releases and released assets
<!-- ## Sync Semantics
Each mirror group is treated as a set of equivalent namespaces. Repositories are matched by repository name across all endpoints.
@@ -238,6 +243,8 @@ Conflict resolution strategies are configured per mirror group:
- `pull_request`: push temporary `refray/conflicts/...` branches and open provider pull requests/merge requests so a person can resolve the divergence.
- `auto_rebase_pull_request`: try `auto_rebase` first, then fall back to pull requests if rebase hits a conflict.
GitLab protected branches reject force-pushes unless the protected branch rule allows them. By default, `refray` temporarily enables GitLab project-level force-push permission for protected branches it already knows it must force-push, then restores the prior setting immediately afterward. Set `allow_temporary_gitlab_force_push = false` on a mirror group to disable this behavior.
When a previously opened conflict pull request is merged, the next sync sees the merged branch as the winning tip, pushes it to the other endpoints, and closes stale `refray/conflicts/...` pull requests for that branch.
Force-pushes are propagated only when `refray` can infer intent from the previous successful sync state. If a branch previously matched everywhere, one endpoint rewrites that branch to a non-descendant tip, and the other endpoints still have the previous tip, `refray` writes local backup refs and a bundle under the work-dir `backups/` directory before force-pushing the rewritten tip to the other endpoints. If multiple endpoints rewrite the branch differently, or another endpoint also advances independently, the branch is treated as a conflict and skipped.
+6
View File
@@ -68,6 +68,8 @@ pub struct MirrorConfig {
pub visibility: Visibility,
#[serde(default)]
pub conflict_resolution: ConflictResolutionStrategy,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub allow_temporary_gitlab_force_push: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
@@ -195,6 +197,10 @@ fn default_true() -> bool {
true
}
fn is_true(value: &bool) -> bool {
*value
}
fn default_jobs() -> usize {
DEFAULT_JOBS
}
+1
View File
@@ -129,6 +129,7 @@ fn add_sync_group_styled(config: &mut Config, theme: &ColorfulTheme) -> Result<(
delete_missing,
visibility: Visibility::Private,
conflict_resolution,
allow_temporary_gitlab_force_push: true,
});
prompt_webhook_setup_styled(config, theme)?;
+50
View File
@@ -40,6 +40,11 @@ pub struct PullRequestInfo {
pub url: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
struct GitlabProtectedBranch {
allow_force_push: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebhookInstallOutcome {
Created,
@@ -185,6 +190,38 @@ impl<'a> ProviderClient<'a> {
)
}
pub fn gitlab_protected_branch_allow_force_push(
&self,
endpoint: &EndpointConfig,
repo_name: &str,
branch: &str,
) -> Result<Option<bool>> {
if self.site.provider != ProviderKind::Gitlab {
bail!("protected branch force-push lookup is only supported for GitLab");
}
let url = self.gitlab_protected_branch_url(endpoint, repo_name, branch);
match self.get_json::<GitlabProtectedBranch>(&url) {
Ok(protected_branch) => Ok(Some(protected_branch.allow_force_push)),
Err(error) if is_not_found_error(&error) => Ok(None),
Err(error) => Err(error),
}
}
pub fn set_gitlab_protected_branch_allow_force_push(
&self,
endpoint: &EndpointConfig,
repo_name: &str,
branch: &str,
allow_force_push: bool,
) -> Result<()> {
if self.site.provider != ProviderKind::Gitlab {
bail!("protected branch force-push update is only supported for GitLab");
}
let url = self.gitlab_protected_branch_url(endpoint, repo_name, branch);
self.patch_json::<serde_json::Value>(&url, &json!({ "allow_force_push": allow_force_push }))
.map(|_| ())
}
pub fn install_webhook(
&self,
endpoint: &EndpointConfig,
@@ -913,6 +950,19 @@ impl<'a> ProviderClient<'a> {
)
}
fn gitlab_protected_branch_url(
&self,
endpoint: &EndpointConfig,
repo_name: &str,
branch: &str,
) -> String {
format!(
"{}/protected_branches/{}",
self.gitlab_project_url(endpoint, repo_name),
urlencoding(branch)
)
}
fn gitlab_project_url(&self, endpoint: &EndpointConfig, repo_name: &str) -> String {
let project = format!("{}/{repo_name}", endpoint.namespace);
format!(
+196 -3
View File
@@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result, bail};
use anyhow::{Context, Result, anyhow, bail};
use console::style;
use regex::Regex;
@@ -658,6 +658,19 @@ struct RepoSyncContext<'a> {
jobs: usize,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct GitlabForcePushTarget {
endpoint: EndpointConfig,
branch: String,
}
#[derive(Clone, Debug)]
struct GitlabForcePushToggle {
endpoint: EndpointConfig,
branch: String,
previous_allow_force_push: bool,
}
#[derive(Default)]
struct RepoSyncOutcome {
state_update: Option<RepoStateUpdate>,
@@ -1548,13 +1561,27 @@ fn push_repo_refs(
close_resolved_pull_requests(context, mirror_repo, remotes, repos, &pushed_branch_names)?;
}
if !rebased_branch_updates.is_empty() {
mirror_repo.push_branch_updates(remotes, &rebased_branch_updates)?;
push_branch_updates_with_temporary_gitlab_force_push(
context,
mirror_repo,
repo_name,
remotes,
repos,
&rebased_branch_updates,
)?;
close_resolved_pull_requests(context, mirror_repo, remotes, repos, &rebased_branch_names)?;
}
if !force_push_updates.is_empty() {
print_branch_force_pushes(&force_pushes);
backup_force_pushed_branches(context, mirror_repo, repo_name, &force_pushes, current_refs)?;
mirror_repo.push_branch_updates(remotes, &force_push_updates)?;
push_branch_updates_with_temporary_gitlab_force_push(
context,
mirror_repo,
repo_name,
remotes,
repos,
&force_push_updates,
)?;
close_resolved_pull_requests(
context,
mirror_repo,
@@ -1800,6 +1827,172 @@ fn force_push_updates(force_pushes: &[BranchForcePush]) -> Vec<BranchUpdate> {
.collect()
}
fn push_branch_updates_with_temporary_gitlab_force_push(
context: &RepoSyncContext<'_>,
mirror_repo: &GitMirror,
repo_name: &str,
remotes: &[RemoteSpec],
repos: &[EndpointRepo],
updates: &[BranchUpdate],
) -> Result<()> {
let toggles = enable_temporary_gitlab_force_push(context, repo_name, repos, updates)?;
let push_result = mirror_repo.push_branch_updates(remotes, updates);
let restore_result = restore_temporary_gitlab_force_push(context, repo_name, &toggles);
match (push_result, restore_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) => Err(error),
(Ok(()), Err(error)) => Err(error),
(Err(push_error), Err(restore_error)) => Err(anyhow!(
"git push failed and failed to restore temporary GitLab force-push permissions: {push_error:#}; restore error: {restore_error:#}"
)),
}
}
fn enable_temporary_gitlab_force_push(
context: &RepoSyncContext<'_>,
repo_name: &str,
repos: &[EndpointRepo],
updates: &[BranchUpdate],
) -> Result<Vec<GitlabForcePushToggle>> {
if !context.mirror.allow_temporary_gitlab_force_push || context.dry_run {
return Ok(Vec::new());
}
let mut toggles = Vec::new();
for target in temporary_gitlab_force_push_targets(context, repos, updates)? {
let result = enable_temporary_gitlab_force_push_for_target(context, repo_name, target);
match result {
Ok(Some(toggle)) => toggles.push(toggle),
Ok(None) => {}
Err(error) => {
let restore_result =
restore_temporary_gitlab_force_push(context, repo_name, &toggles);
return match restore_result {
Ok(()) => Err(error),
Err(restore_error) => Err(anyhow!(
"failed to enable temporary GitLab force-push permissions: {error:#}; restore error: {restore_error:#}"
)),
};
}
}
}
Ok(toggles)
}
fn enable_temporary_gitlab_force_push_for_target(
context: &RepoSyncContext<'_>,
repo_name: &str,
target: GitlabForcePushTarget,
) -> Result<Option<GitlabForcePushToggle>> {
let site = context.config.site(&target.endpoint.site).unwrap();
let client = ProviderClient::new(site)?;
let label = target.endpoint.label();
match client
.gitlab_protected_branch_allow_force_push(&target.endpoint, repo_name, &target.branch)
.with_context(|| {
format!(
"failed to inspect GitLab protected branch {} on {}",
target.branch, label
)
})? {
None | Some(true) => Ok(None),
Some(false) => {
crate::logln!(
" {} branch {} on {}",
style("allow force-push").cyan().bold(),
style(&target.branch).cyan(),
style(&label).dim()
);
client
.set_gitlab_protected_branch_allow_force_push(
&target.endpoint,
repo_name,
&target.branch,
true,
)
.with_context(|| {
format!(
"failed to allow GitLab force-push for branch {} on {}",
target.branch, label
)
})?;
Ok(Some(GitlabForcePushToggle {
endpoint: target.endpoint,
branch: target.branch,
previous_allow_force_push: false,
}))
}
}
}
fn restore_temporary_gitlab_force_push(
context: &RepoSyncContext<'_>,
repo_name: &str,
toggles: &[GitlabForcePushToggle],
) -> Result<()> {
let mut failures = Vec::new();
for toggle in toggles.iter().rev() {
let site = context.config.site(&toggle.endpoint.site).unwrap();
let label = toggle.endpoint.label();
crate::logln!(
" {} branch {} on {}",
style("restore force-push").cyan().bold(),
style(&toggle.branch).cyan(),
style(&label).dim()
);
let result = ProviderClient::new(site).and_then(|client| {
client.set_gitlab_protected_branch_allow_force_push(
&toggle.endpoint,
repo_name,
&toggle.branch,
toggle.previous_allow_force_push,
)
});
if let Err(error) = result {
failures.push(format!("branch {} on {}: {error:#}", toggle.branch, label));
}
}
if failures.is_empty() {
Ok(())
} else {
bail!(
"failed to restore temporary GitLab force-push permissions: {}",
failures.join("; ")
)
}
}
fn temporary_gitlab_force_push_targets(
context: &RepoSyncContext<'_>,
repos: &[EndpointRepo],
updates: &[BranchUpdate],
) -> Result<Vec<GitlabForcePushTarget>> {
let repos_by_remote = endpoint_repos_by_remote_name(context, repos)?;
let mut seen = BTreeSet::new();
let mut targets = Vec::new();
for update in updates.iter().filter(|update| update.force) {
let Some(endpoint_repo) = repos_by_remote.get(&update.target_remote) else {
continue;
};
let site = context.config.site(&endpoint_repo.endpoint.site).unwrap();
if site.provider != ProviderKind::Gitlab {
continue;
}
let target = GitlabForcePushTarget {
endpoint: endpoint_repo.endpoint.clone(),
branch: update.branch.clone(),
};
if seen.insert(target.clone()) {
targets.push(target);
}
}
Ok(targets)
}
fn open_conflict_pull_requests(
context: &RepoSyncContext<'_>,
mirror_repo: &GitMirror,
+48
View File
@@ -59,6 +59,7 @@ fn parses_value_tokens() {
Some("-archive$".to_string())
);
assert!(!config.mirrors[0].delete_missing);
assert!(config.mirrors[0].allow_temporary_gitlab_force_push);
let webhook = config.webhook.unwrap();
assert!(webhook.install);
assert_eq!(webhook.url, "https://mirror.example.test/webhook");
@@ -116,6 +117,49 @@ fn mirror_defaults_to_deleting_missing_repos_for_existing_configs() {
.unwrap();
assert!(config.mirrors[0].delete_missing);
assert!(config.mirrors[0].allow_temporary_gitlab_force_push);
}
#[test]
fn mirror_can_disable_temporary_gitlab_force_push() {
let config: Config = toml::from_str(
r#"
[[mirrors]]
name = "personal"
allow_temporary_gitlab_force_push = false
[[mirrors.endpoints]]
site = "github"
kind = "user"
namespace = "alice"
[[mirrors.endpoints]]
site = "gitlab"
kind = "group"
namespace = "acme"
"#,
)
.unwrap();
assert!(!config.mirrors[0].allow_temporary_gitlab_force_push);
}
#[test]
fn mirror_serializes_temporary_gitlab_force_push_opt_out() {
let mut mirror = mirror_config();
mirror.allow_temporary_gitlab_force_push = false;
let config = Config {
jobs: crate::config::DEFAULT_JOBS,
sites: vec![site("github", ProviderKind::Github)],
mirrors: vec![mirror],
webhook: None,
};
let encoded = toml::to_string(&config).unwrap();
let decoded: Config = toml::from_str(&encoded).unwrap();
assert!(encoded.contains("allow_temporary_gitlab_force_push = false"));
assert!(!decoded.mirrors[0].allow_temporary_gitlab_force_push);
}
#[test]
@@ -137,6 +181,7 @@ fn validation_rejects_unknown_sites_and_single_endpoint_groups() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -167,6 +212,7 @@ fn validation_rejects_unknown_sites_and_single_endpoint_groups() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -272,6 +318,7 @@ fn validation_rejects_duplicate_mirror_endpoints() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -319,6 +366,7 @@ fn mirror_config() -> MirrorConfig {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}
}
+6
View File
@@ -258,6 +258,7 @@ fn wizard_starts_existing_config_at_sync_group_menu() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -288,6 +289,7 @@ fn wizard_can_ask_to_run_full_sync_after_config() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -365,6 +367,7 @@ fn wizard_edits_existing_sync_group_from_menu() {
delete_missing: true,
visibility: Visibility::Public,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -456,6 +459,7 @@ fn wizard_prefills_existing_sync_group_when_editing() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -521,6 +525,7 @@ fn wizard_deletes_existing_sync_group_from_menu() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -581,6 +586,7 @@ fn wizard_can_go_back_from_delete_menu() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
+1
View File
@@ -79,6 +79,7 @@ where
delete_missing,
visibility: Visibility::Private,
conflict_resolution,
allow_temporary_gitlab_force_push: true,
});
prompt_webhook_setup(reader, writer, config)?;
Ok(())
+113
View File
@@ -686,6 +686,119 @@ fn delete_repo_deletes_url_encoded_gitlab_project() {
handle.join().unwrap();
}
#[test]
fn gitlab_protected_branch_allow_force_push_reads_setting() {
let (api_url, handle) = one_request_server(
"200 OK",
r#"{"name":"main","allow_force_push":false}"#,
|request| {
assert!(
request.starts_with("GET /projects/parent%2Falice%2Frepo/protected_branches/main "),
"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)
};
let allow = ProviderClient::new(&site)
.unwrap()
.gitlab_protected_branch_allow_force_push(
&EndpointConfig {
site: "gitlab".to_string(),
kind: NamespaceKind::Group,
namespace: "parent/alice".to_string(),
},
"repo",
"main",
)
.unwrap();
assert_eq!(allow, Some(false));
handle.join().unwrap();
}
#[test]
fn gitlab_protected_branch_allow_force_push_treats_404_as_unprotected() {
let (api_url, handle) = one_request_server(
"404 Not Found",
r#"{"message":"404 Not found"}"#,
|request| {
assert!(
request
.starts_with("GET /projects/alice%2Frepo/protected_branches/feature%2Fsync "),
"request was {request}"
);
},
);
let site = SiteConfig {
api_url: Some(api_url),
..site(ProviderKind::Gitlab, None)
};
let allow = ProviderClient::new(&site)
.unwrap()
.gitlab_protected_branch_allow_force_push(
&EndpointConfig {
site: "gitlab".to_string(),
kind: NamespaceKind::User,
namespace: "alice".to_string(),
},
"repo",
"feature/sync",
)
.unwrap();
assert_eq!(allow, None);
handle.join().unwrap();
}
#[test]
fn set_gitlab_protected_branch_allow_force_push_patches_branch() {
let (api_url, handle) = one_request_server(
"200 OK",
r#"{"name":"main","allow_force_push":true}"#,
|request| {
assert!(
request
.starts_with("PATCH /projects/parent%2Falice%2Frepo/protected_branches/main "),
"request was {request}"
);
assert!(
request.contains(r#"{"allow_force_push":true}"#),
"request was {request}"
);
},
);
let site = SiteConfig {
api_url: Some(api_url),
..site(ProviderKind::Gitlab, None)
};
ProviderClient::new(&site)
.unwrap()
.set_gitlab_protected_branch_allow_force_push(
&EndpointConfig {
site: "gitlab".to_string(),
kind: NamespaceKind::Group,
namespace: "parent/alice".to_string(),
},
"repo",
"main",
true,
)
.unwrap();
handle.join().unwrap();
}
#[test]
fn delete_repo_deletes_gitea_repo() {
let (api_url, handle) = one_request_server("204 No Content", "", |request| {
+80
View File
@@ -448,6 +448,73 @@ fn endpoint_remote_names_do_not_slug_collide() {
);
}
#[test]
fn temporary_gitlab_force_push_targets_selects_only_gitlab_force_updates() {
let mirror = MirrorConfig {
endpoints: vec![endpoint("github"), endpoint("gitlab"), endpoint("gitea")],
..test_mirror()
};
let config = Config {
jobs: crate::config::DEFAULT_JOBS,
sites: vec![
site_config("github", crate::config::ProviderKind::Github),
site_config("gitlab", crate::config::ProviderKind::Gitlab),
site_config("gitea", crate::config::ProviderKind::Gitea),
],
mirrors: vec![mirror.clone()],
webhook: None,
};
let context = RepoSyncContext {
config: &config,
mirror: &mirror,
work_dir: Path::new("."),
redactor: Redactor::new(Vec::new()),
dry_run: false,
jobs: crate::config::DEFAULT_JOBS,
};
let updates = vec![
BranchUpdate {
branch: "main".to_string(),
sha: "a".repeat(40),
target_remote: remote_key("gitlab"),
force: true,
},
BranchUpdate {
branch: "main".to_string(),
sha: "a".repeat(40),
target_remote: remote_key("gitlab"),
force: true,
},
BranchUpdate {
branch: "feature".to_string(),
sha: "b".repeat(40),
target_remote: remote_key("gitlab"),
force: false,
},
BranchUpdate {
branch: "main".to_string(),
sha: "c".repeat(40),
target_remote: remote_key("github"),
force: true,
},
];
let repos = vec![
endpoint_repo("github"),
endpoint_repo("gitlab"),
endpoint_repo("gitea"),
];
let targets = temporary_gitlab_force_push_targets(&context, &repos, &updates).unwrap();
assert_eq!(
targets,
vec![GitlabForcePushTarget {
endpoint: endpoint("gitlab"),
branch: "main".to_string(),
}]
);
}
#[test]
fn targeted_endpoint_repos_synthesize_clone_urls_without_listing() {
let mirror = MirrorConfig {
@@ -464,6 +531,7 @@ fn targeted_endpoint_repos_synthesize_clone_urls_without_listing() {
delete_missing: true,
visibility: crate::config::Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
};
let config = Config {
jobs: crate::config::DEFAULT_JOBS,
@@ -603,6 +671,7 @@ fn test_mirror() -> MirrorConfig {
delete_missing: true,
visibility: crate::config::Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}
}
@@ -618,6 +687,17 @@ fn remote_key(site: &str) -> String {
remote_name_for_endpoint(&endpoint(site))
}
fn site_config(name: &str, provider: crate::config::ProviderKind) -> crate::config::SiteConfig {
crate::config::SiteConfig {
name: name.to_string(),
provider,
base_url: format!("https://{name}.invalid"),
api_url: None,
token: crate::config::TokenConfig::Value("token".to_string()),
git_username: None,
}
}
fn endpoint_repo(site: &str) -> EndpointRepo {
EndpointRepo {
endpoint: endpoint(site),
+4
View File
@@ -118,6 +118,7 @@ fn matches_jobs_by_provider_and_namespace() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -146,6 +147,7 @@ fn matching_jobs_respects_repo_name_filters() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
};
let config = Config {
jobs: crate::config::DEFAULT_JOBS,
@@ -365,6 +367,7 @@ fn uninstall_webhooks_skips_blocked_provider_access() {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}],
webhook: None,
};
@@ -719,6 +722,7 @@ fn filtered_mirror() -> MirrorConfig {
delete_missing: true,
visibility: Visibility::Private,
conflict_resolution: ConflictResolutionStrategy::Fail,
allow_temporary_gitlab_force_push: true,
}
}