[F] Name validation
This commit is contained in:
@@ -12,13 +12,15 @@ 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**: Delete branches/repos across platforms when they are deleted from one platform
|
||||
- **tracks deletions**: Branches/repo deletions 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!
|
||||
|
||||
Supported platforms: GitHub, GitLab, Gitea, Forgejo
|
||||
|
||||
> [!NOTE]
|
||||
> Meow
|
||||
> My cat made this codebase, meow
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
@@ -177,6 +179,10 @@ To move installed hooks to a new public URL, use `webhook update`. It removes ho
|
||||
refray webhook update https://new.example.com/webhook
|
||||
```
|
||||
|
||||
## Issues and Pull Requests
|
||||
|
||||
Issues and pull requests are not mirrored.
|
||||
|
||||
## Sync Semantics
|
||||
|
||||
Each mirror group is treated as a set of equivalent namespaces. Repositories are matched by repository name across all endpoints.
|
||||
@@ -245,6 +251,3 @@ REFRAY_E2E_ALLOW_DESTRUCTIVE=1 \
|
||||
|
||||
By default cleanup only deletes repositories named `refray-e2e-*`. To start by deleting every owned repository visible to the configured accounts, set `REFRAY_E2E_CLEAR_ALL_REPOS=DELETE_ALL_OWNED_REPOS`. Provider skips (`REFRAY_E2E_SKIP_GITHUB`, `REFRAY_E2E_SKIP_GITLAB`, `REFRAY_E2E_SKIP_GITEA`, `REFRAY_E2E_SKIP_FORGEJO`) and `REFRAY_E2E_ALLOW_PARTIAL=1` are available for local debugging, but the full support check should run with all four providers.
|
||||
|
||||
## Issues and Pull Requests
|
||||
|
||||
Issues and pull requests are not mirrored.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 433 KiB |
+75
-13
@@ -11,7 +11,7 @@ use regex::Regex;
|
||||
|
||||
use crate::config::{
|
||||
Config, ConflictResolutionStrategy, DEFAULT_JOBS, EndpointConfig, MirrorConfig, NamespaceKind,
|
||||
RepoNameFilter, SyncVisibility, Visibility, default_work_dir, validate_config,
|
||||
ProviderKind, RepoNameFilter, SyncVisibility, Visibility, default_work_dir, validate_config,
|
||||
};
|
||||
use crate::git::{
|
||||
BranchConflict, BranchDeletion, BranchUpdate, GitMirror, Redactor, RefBackup, RemoteSpec,
|
||||
@@ -530,7 +530,13 @@ fn ensure_missing_repos(
|
||||
let description = template.and_then(|repo| repo.description);
|
||||
let expected_private = matches!(create_visibility, Visibility::Private);
|
||||
let create_jobs = missing.into_iter().enumerate().collect::<Vec<_>>();
|
||||
let mut created = crate::parallel::map(create_jobs, context.jobs, |(index, endpoint)| {
|
||||
let created = crate::parallel::map(create_jobs, context.jobs, |(index, endpoint)| {
|
||||
let site = context.config.site(&endpoint.site).unwrap();
|
||||
if site.provider == ProviderKind::Gitlab && !is_supported_gitlab_project_path(repo_name) {
|
||||
log_invalid_gitlab_project_name_skip(repo_name, &endpoint);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
crate::logln!(
|
||||
" {} {} {}",
|
||||
style("create").green().bold(),
|
||||
@@ -538,16 +544,27 @@ fn ensure_missing_repos(
|
||||
style(format!("on {}", endpoint.label())).dim()
|
||||
);
|
||||
|
||||
let site = context.config.site(&endpoint.site).unwrap();
|
||||
let client = ProviderClient::new(site)?;
|
||||
let created = client
|
||||
.create_repo(
|
||||
&endpoint,
|
||||
repo_name,
|
||||
&create_visibility,
|
||||
description.as_deref(),
|
||||
)
|
||||
.with_context(|| format!("failed to create {} on {}", repo_name, endpoint.label()))?;
|
||||
let created = match client.create_repo(
|
||||
&endpoint,
|
||||
repo_name,
|
||||
&create_visibility,
|
||||
description.as_deref(),
|
||||
) {
|
||||
Ok(created) => created,
|
||||
Err(error)
|
||||
if site.provider == ProviderKind::Gitlab
|
||||
&& is_gitlab_invalid_project_name_error(&error) =>
|
||||
{
|
||||
log_invalid_gitlab_project_name_skip(repo_name, &endpoint);
|
||||
return Ok(None);
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(error).with_context(|| {
|
||||
format!("failed to create {} on {}", repo_name, endpoint.label())
|
||||
});
|
||||
}
|
||||
};
|
||||
if created.private != expected_private {
|
||||
crate::logln!(
|
||||
" {} created {} on {}, but provider reported a different visibility than requested",
|
||||
@@ -556,14 +573,15 @@ fn ensure_missing_repos(
|
||||
style(endpoint.label()).dim()
|
||||
);
|
||||
}
|
||||
Ok((
|
||||
Ok(Some((
|
||||
index,
|
||||
EndpointRepo {
|
||||
endpoint,
|
||||
repo: created,
|
||||
},
|
||||
))
|
||||
)))
|
||||
})?;
|
||||
let mut created = created.into_iter().flatten().collect::<Vec<_>>();
|
||||
created.sort_by_key(|(index, _)| *index);
|
||||
let created = created
|
||||
.into_iter()
|
||||
@@ -586,6 +604,50 @@ fn visibility_for_created_repo(mirror: &MirrorConfig, template: Option<&RemoteRe
|
||||
.unwrap_or_else(|| mirror.visibility.clone())
|
||||
}
|
||||
|
||||
fn is_supported_gitlab_project_path(name: &str) -> bool {
|
||||
if name.is_empty()
|
||||
|| matches!(name.chars().next(), Some('-' | '_' | '.'))
|
||||
|| matches!(name.chars().last(), Some('-' | '_' | '.'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let lower = name.to_ascii_lowercase();
|
||||
if lower.ends_with(".git") || lower.ends_with(".atom") {
|
||||
return false;
|
||||
}
|
||||
|
||||
name.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
|
||||
}
|
||||
|
||||
fn log_invalid_gitlab_project_name_skip(repo_name: &str, endpoint: &EndpointConfig) {
|
||||
crate::logln!(
|
||||
" {} {} {}",
|
||||
style("skip").yellow().bold(),
|
||||
style(repo_name).cyan(),
|
||||
style(format!(
|
||||
"on {}: invalid GitLab project name/path",
|
||||
endpoint.label()
|
||||
))
|
||||
.dim()
|
||||
);
|
||||
}
|
||||
|
||||
fn is_gitlab_invalid_project_name_error(error: &anyhow::Error) -> bool {
|
||||
let text = error
|
||||
.chain()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
.to_ascii_lowercase();
|
||||
text.contains("400 bad request")
|
||||
&& (text.contains("project_namespace.path")
|
||||
|| text.contains("can only include non-accented letters")
|
||||
|| text.contains("must not start with")
|
||||
|| text.contains("must start with a letter"))
|
||||
}
|
||||
|
||||
struct RepoSyncContext<'a> {
|
||||
config: &'a Config,
|
||||
mirror: &'a MirrorConfig,
|
||||
|
||||
@@ -532,6 +532,39 @@ fn created_repo_visibility_falls_back_to_config_without_template() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitlab_invalid_project_name_errors_are_skippable() {
|
||||
let error = anyhow::Error::msg(
|
||||
r#"POST https://gitlab.com/api/v4/projects returned 400 Bad Request: {"message":{"project_namespace.path":["can only include non-accented letters, digits, '_', '-' and '.'. It must not start with '-', '_', or '.'."],"name":["can contain only letters, digits, emoji, '_', '.', '+', dashes, or spaces. It must start with a letter, digit, emoji, or '_'."]}}"#,
|
||||
);
|
||||
|
||||
assert!(is_gitlab_invalid_project_name_error(&error));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitlab_project_path_validation_matches_create_constraints() {
|
||||
for name in ["Kairos", "needLe", "amaoke.app", "repo_1", "repo-1"] {
|
||||
assert!(is_supported_gitlab_project_path(name), "{name}");
|
||||
}
|
||||
|
||||
for name in [
|
||||
"",
|
||||
".github",
|
||||
"_private",
|
||||
"-draft",
|
||||
"repo.",
|
||||
"repo_",
|
||||
"repo-",
|
||||
"repo.git",
|
||||
"repo.atom",
|
||||
"has space",
|
||||
"has+plus",
|
||||
"荞麦main",
|
||||
] {
|
||||
assert!(!is_supported_gitlab_project_path(name), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_ref_state(hash: &str, branches: &[(&str, &str)]) -> RemoteRefState {
|
||||
RemoteRefState {
|
||||
hash: hash.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user