[F] Visibility sync
This commit is contained in:
@@ -177,7 +177,7 @@ refray webhook update https://new.example.com/webhook
|
|||||||
|
|
||||||
Each mirror group is treated as a set of equivalent namespaces. Repositories are matched by repository name across all endpoints.
|
Each mirror group is treated as a set of equivalent namespaces. Repositories are matched by repository name across all endpoints.
|
||||||
|
|
||||||
Set `sync_visibility = "all"`, `"private"`, or `"public"` on a mirror group to choose which repository visibility is included in that group. `visibility` still controls the visibility used when `refray` creates a missing repository.
|
Set `sync_visibility = "all"`, `"private"`, or `"public"` on a mirror group to choose which repository visibility is included in that group. When `refray` creates a missing repository, it mirrors the visibility of the existing repository it is syncing from; `visibility` is only a fallback when no source visibility is available.
|
||||||
|
|
||||||
Set `repo_whitelist = ["..."]` and/or `repo_blacklist = ["..."]` on a mirror group to filter repository names with regular expressions. An empty whitelist includes all repository names, and blacklist matches are excluded after whitelist matches. These name filters are independent from `sync_visibility`; both must match for a repository to be synced.
|
Set `repo_whitelist = ["..."]` and/or `repo_blacklist = ["..."]` on a mirror group to filter repository names with regular expressions. An empty whitelist includes all repository names, and blacklist matches are excluded after whitelist matches. These name filters are independent from `sync_visibility`; both must match for a repository to be synced.
|
||||||
|
|
||||||
|
|||||||
+17
-7
@@ -10,7 +10,7 @@ use regex::Regex;
|
|||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
Config, ConflictResolutionStrategy, DEFAULT_JOBS, EndpointConfig, MirrorConfig, NamespaceKind,
|
Config, ConflictResolutionStrategy, DEFAULT_JOBS, EndpointConfig, MirrorConfig, NamespaceKind,
|
||||||
RepoNameFilter, SyncVisibility, default_work_dir, validate_config,
|
RepoNameFilter, SyncVisibility, Visibility, default_work_dir, validate_config,
|
||||||
};
|
};
|
||||||
use crate::git::{
|
use crate::git::{
|
||||||
BranchConflict, BranchDeletion, BranchUpdate, GitMirror, Redactor, RemoteSpec,
|
BranchConflict, BranchDeletion, BranchUpdate, GitMirror, Redactor, RemoteSpec,
|
||||||
@@ -18,7 +18,7 @@ use crate::git::{
|
|||||||
};
|
};
|
||||||
use crate::logging;
|
use crate::logging;
|
||||||
use crate::provider::{
|
use crate::provider::{
|
||||||
EndpointRepo, ProviderClient, PullRequestRequest, list_mirror_repos, repos_by_name,
|
EndpointRepo, ProviderClient, PullRequestRequest, RemoteRepo, list_mirror_repos, repos_by_name,
|
||||||
};
|
};
|
||||||
use crate::webhook;
|
use crate::webhook;
|
||||||
|
|
||||||
@@ -425,6 +425,7 @@ fn ensure_missing_repos(
|
|||||||
.filter(|endpoint| !present.contains(*endpoint))
|
.filter(|endpoint| !present.contains(*endpoint))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let create_visibility = visibility_for_created_repo(context.mirror, template.as_ref());
|
||||||
|
|
||||||
if !create_missing || context.dry_run {
|
if !create_missing || context.dry_run {
|
||||||
for endpoint in &missing {
|
for endpoint in &missing {
|
||||||
@@ -449,10 +450,7 @@ fn ensure_missing_repos(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let description = template.and_then(|repo| repo.description);
|
let description = template.and_then(|repo| repo.description);
|
||||||
let expected_private = matches!(
|
let expected_private = matches!(create_visibility, Visibility::Private);
|
||||||
&context.mirror.visibility,
|
|
||||||
crate::config::Visibility::Private
|
|
||||||
);
|
|
||||||
let create_jobs = missing.into_iter().enumerate().collect::<Vec<_>>();
|
let create_jobs = missing.into_iter().enumerate().collect::<Vec<_>>();
|
||||||
let mut created = crate::parallel::map(create_jobs, context.jobs, |(index, endpoint)| {
|
let mut created = crate::parallel::map(create_jobs, context.jobs, |(index, endpoint)| {
|
||||||
crate::logln!(
|
crate::logln!(
|
||||||
@@ -468,7 +466,7 @@ fn ensure_missing_repos(
|
|||||||
.create_repo(
|
.create_repo(
|
||||||
&endpoint,
|
&endpoint,
|
||||||
repo_name,
|
repo_name,
|
||||||
&context.mirror.visibility,
|
&create_visibility,
|
||||||
description.as_deref(),
|
description.as_deref(),
|
||||||
)
|
)
|
||||||
.with_context(|| format!("failed to create {} on {}", repo_name, endpoint.label()))?;
|
.with_context(|| format!("failed to create {} on {}", repo_name, endpoint.label()))?;
|
||||||
@@ -494,6 +492,18 @@ fn ensure_missing_repos(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visibility_for_created_repo(mirror: &MirrorConfig, template: Option<&RemoteRepo>) -> Visibility {
|
||||||
|
template
|
||||||
|
.map(|repo| {
|
||||||
|
if repo.private {
|
||||||
|
Visibility::Private
|
||||||
|
} else {
|
||||||
|
Visibility::Public
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| mirror.visibility.clone())
|
||||||
|
}
|
||||||
|
|
||||||
struct RepoSyncContext<'a> {
|
struct RepoSyncContext<'a> {
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
mirror: &'a MirrorConfig,
|
mirror: &'a MirrorConfig,
|
||||||
|
|||||||
+98
-3
@@ -38,6 +38,8 @@ fn sequential_live_e2e_all_supported_features() -> Result<()> {
|
|||||||
run.creation_branch_tag_and_read_write_sync()?;
|
run.creation_branch_tag_and_read_write_sync()?;
|
||||||
eprintln!("e2e phase: dry-run/no-create");
|
eprintln!("e2e phase: dry-run/no-create");
|
||||||
run.dry_run_and_no_create_do_not_write()?;
|
run.dry_run_and_no_create_do_not_write()?;
|
||||||
|
eprintln!("e2e phase: visibility sync");
|
||||||
|
run.repository_visibility_is_mirrored()?;
|
||||||
eprintln!("e2e phase: retry failed");
|
eprintln!("e2e phase: retry failed");
|
||||||
run.failed_sync_can_retry_only_failed_repo()?;
|
run.failed_sync_can_retry_only_failed_repo()?;
|
||||||
eprintln!("e2e phase: auto rebase");
|
eprintln!("e2e phase: auto rebase");
|
||||||
@@ -427,6 +429,26 @@ namespace = "{}"
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn repository_visibility_is_mirrored(&self) -> Result<()> {
|
||||||
|
let source = self.primary_provider();
|
||||||
|
|
||||||
|
let public_repo = self.repo_name("visibility-public");
|
||||||
|
source.create_repo_with_visibility(&public_repo, false)?;
|
||||||
|
self.seed_main(source, &public_repo, "visibility public", 1_700_000_251)?;
|
||||||
|
self.set_mirror_visibility("private")?;
|
||||||
|
self.sync_repo(&public_repo, [])?;
|
||||||
|
self.assert_repo_visibility_all(&public_repo, false)?;
|
||||||
|
|
||||||
|
let private_repo = self.repo_name("visibility-private");
|
||||||
|
source.create_repo_with_visibility(&private_repo, true)?;
|
||||||
|
self.seed_main(source, &private_repo, "visibility private", 1_700_000_252)?;
|
||||||
|
self.set_mirror_visibility("public")?;
|
||||||
|
self.sync_repo(&private_repo, [])?;
|
||||||
|
self.assert_repo_visibility_all(&private_repo, true)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn failed_sync_can_retry_only_failed_repo(&self) -> Result<()> {
|
fn failed_sync_can_retry_only_failed_repo(&self) -> Result<()> {
|
||||||
let repo = self.repo_name("retry");
|
let repo = self.repo_name("retry");
|
||||||
let (source, peer) = self.provider_pair();
|
let (source, peer) = self.provider_pair();
|
||||||
@@ -790,6 +812,33 @@ namespace = "{}"
|
|||||||
.with_context(|| format!("failed to write {}", self.config_path.display()))
|
.with_context(|| format!("failed to write {}", self.config_path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_mirror_visibility(&self, visibility: &str) -> Result<()> {
|
||||||
|
let contents = fs::read_to_string(&self.config_path)
|
||||||
|
.with_context(|| format!("failed to read {}", self.config_path.display()))?;
|
||||||
|
let replacement = format!("visibility = \"{visibility}\"");
|
||||||
|
let mut replaced = false;
|
||||||
|
let mut updated = contents
|
||||||
|
.lines()
|
||||||
|
.map(|line| {
|
||||||
|
if line.starts_with("visibility = ") {
|
||||||
|
replaced = true;
|
||||||
|
replacement.clone()
|
||||||
|
} else {
|
||||||
|
line.to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
if contents.ends_with('\n') {
|
||||||
|
updated.push('\n');
|
||||||
|
}
|
||||||
|
if !replaced {
|
||||||
|
bail!("config is missing mirror visibility");
|
||||||
|
}
|
||||||
|
fs::write(&self.config_path, updated)
|
||||||
|
.with_context(|| format!("failed to write {}", self.config_path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
fn set_webhook_url(&self, url: &str) -> Result<()> {
|
fn set_webhook_url(&self, url: &str) -> Result<()> {
|
||||||
let contents = fs::read_to_string(&self.config_path)
|
let contents = fs::read_to_string(&self.config_path)
|
||||||
.with_context(|| format!("failed to read {}", self.config_path.display()))?;
|
.with_context(|| format!("failed to read {}", self.config_path.display()))?;
|
||||||
@@ -963,6 +1012,21 @@ namespace = "{}"
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_repo_visibility_all(&self, repo: &str, private: bool) -> Result<()> {
|
||||||
|
retry("repo visibility", || {
|
||||||
|
for provider in &self.settings.providers {
|
||||||
|
let actual = provider.repo_private(repo)?;
|
||||||
|
if actual != private {
|
||||||
|
bail!(
|
||||||
|
"expected {repo} private={private} on {}, got private={actual}",
|
||||||
|
provider.site_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_branch_all_equal(&self, repo: &str, branch: &str) -> Result<()> {
|
fn assert_branch_all_equal(&self, repo: &str, branch: &str) -> Result<()> {
|
||||||
retry("branch convergence", || {
|
retry("branch convergence", || {
|
||||||
let refs = self.refs_by_provider(repo)?;
|
let refs = self.refs_by_provider(repo)?;
|
||||||
@@ -1274,6 +1338,29 @@ impl ProviderAccount {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn repo_private(&self, repo: &str) -> Result<bool> {
|
||||||
|
let url = self.repo_api_url(repo);
|
||||||
|
let response = self.auth(self.http.get(url.clone())).send()?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
check_response("GET", &url, response)?;
|
||||||
|
unreachable!("check_response returns Err for unsuccessful responses")
|
||||||
|
}
|
||||||
|
let value: Value = response
|
||||||
|
.json()
|
||||||
|
.with_context(|| format!("invalid JSON from {url}"))?;
|
||||||
|
match self.kind {
|
||||||
|
ProviderKind::Github | ProviderKind::Gitea | ProviderKind::Forgejo => value
|
||||||
|
.get("private")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.ok_or_else(|| anyhow!("{} repo response missing private", self.site_name)),
|
||||||
|
ProviderKind::Gitlab => value
|
||||||
|
.get("visibility")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|visibility| visibility != "public")
|
||||||
|
.ok_or_else(|| anyhow!("{} repo response missing visibility", self.site_name)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn wait_repo_present(&self, repo: &str) -> Result<()> {
|
fn wait_repo_present(&self, repo: &str) -> Result<()> {
|
||||||
retry("repo present", || {
|
retry("repo present", || {
|
||||||
if self.repo_exists(repo)? {
|
if self.repo_exists(repo)? {
|
||||||
@@ -1329,15 +1416,23 @@ impl ProviderAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_repo(&self, name: &str) -> Result<()> {
|
fn create_repo(&self, name: &str) -> Result<()> {
|
||||||
|
self.create_repo_with_visibility(name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_repo_with_visibility(&self, name: &str, private: bool) -> Result<()> {
|
||||||
if self.repo_exists(name)? {
|
if self.repo_exists(name)? {
|
||||||
self.wait_repo_listed(name)?;
|
self.wait_repo_listed(name)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let body = match self.kind {
|
let body = match self.kind {
|
||||||
ProviderKind::Github => json!({ "name": name, "private": false, "auto_init": false }),
|
ProviderKind::Github => json!({ "name": name, "private": private, "auto_init": false }),
|
||||||
ProviderKind::Gitlab => json!({ "name": name, "path": name, "visibility": "public" }),
|
ProviderKind::Gitlab => json!({
|
||||||
|
"name": name,
|
||||||
|
"path": name,
|
||||||
|
"visibility": if private { "private" } else { "public" },
|
||||||
|
}),
|
||||||
ProviderKind::Gitea | ProviderKind::Forgejo => {
|
ProviderKind::Gitea | ProviderKind::Forgejo => {
|
||||||
json!({ "name": name, "private": false, "auto_init": false })
|
json!({ "name": name, "private": private, "auto_init": false })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let url = match self.kind {
|
let url = match self.kind {
|
||||||
|
|||||||
@@ -396,6 +396,50 @@ fn endpoint_remote_names_do_not_slug_collide() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn created_repo_visibility_follows_existing_public_repo() {
|
||||||
|
let mirror = test_mirror();
|
||||||
|
let repo = crate::provider::RemoteRepo {
|
||||||
|
name: "repo".to_string(),
|
||||||
|
clone_url: "https://github.invalid/alice/repo.git".to_string(),
|
||||||
|
private: false,
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
visibility_for_created_repo(&mirror, Some(&repo)),
|
||||||
|
crate::config::Visibility::Public
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn created_repo_visibility_follows_existing_private_repo() {
|
||||||
|
let mut mirror = test_mirror();
|
||||||
|
mirror.visibility = crate::config::Visibility::Public;
|
||||||
|
let repo = crate::provider::RemoteRepo {
|
||||||
|
name: "repo".to_string(),
|
||||||
|
clone_url: "https://github.invalid/alice/repo.git".to_string(),
|
||||||
|
private: true,
|
||||||
|
description: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
visibility_for_created_repo(&mirror, Some(&repo)),
|
||||||
|
crate::config::Visibility::Private
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn created_repo_visibility_falls_back_to_config_without_template() {
|
||||||
|
let mut mirror = test_mirror();
|
||||||
|
mirror.visibility = crate::config::Visibility::Public;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
visibility_for_created_repo(&mirror, None),
|
||||||
|
crate::config::Visibility::Public
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn remote_ref_state(hash: &str, branches: &[(&str, &str)]) -> RemoteRefState {
|
fn remote_ref_state(hash: &str, branches: &[(&str, &str)]) -> RemoteRefState {
|
||||||
RemoteRefState {
|
RemoteRefState {
|
||||||
hash: hash.to_string(),
|
hash: hash.to_string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user