Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
crates/auth/src/access.rs +1 −0
| @@ -150,6 +150,7 @@ mod tests { | |||
| 150 | object_format: "sha1".to_string(), | 150 | object_format: "sha1".to_string(), |
| 151 | issues_enabled: true, | 151 | issues_enabled: true, |
| 152 | pulls_enabled: true, | 152 | pulls_enabled: true, |
| 153 | is_mirror: false, | ||
| 153 | next_iid: 1, | 154 | next_iid: 1, |
| 154 | archived_at: None, | 155 | archived_at: None, |
| 155 | size_bytes: 0, | 156 | size_bytes: 0, |
crates/model/src/entity.rs +2 −0
| @@ -425,6 +425,8 @@ pub struct Repo { | |||
| 425 | pub issues_enabled: bool, | 425 | pub issues_enabled: bool, |
| 426 | /// Whether pull requests are enabled for this repository. | 426 | /// Whether pull requests are enabled for this repository. |
| 427 | pub pulls_enabled: bool, | 427 | pub pulls_enabled: bool, |
| 428 | /// Whether this repository is a mirror (kept in sync from a remote). | ||
| 429 | pub is_mirror: bool, | ||
| 428 | /// The next issue/PR number to allocate (shared counter). | 430 | /// The next issue/PR number to allocate (shared counter). |
| 429 | pub next_iid: Timestamp, | 431 | pub next_iid: Timestamp, |
| 430 | /// When the repo was archived (read-only), if it is. | 432 | /// When the repo was archived (read-only), if it is. |
crates/store/src/repos.rs +21 −2
| @@ -12,8 +12,8 @@ use crate::{Store, StoreError, map_conflict, new_id, now_ms}; | |||
| 12 | 12 | ||
| 13 | /// The columns of `repos` in a fixed order, shared by every `SELECT`. | 13 | /// The columns of `repos` in a fixed order, shared by every `SELECT`. |
| 14 | const REPO_COLUMNS: &str = "id, owner_id, group_id, name, path, description, visibility, \ | 14 | const REPO_COLUMNS: &str = "id, owner_id, group_id, name, path, description, visibility, \ |
| 15 | default_branch, object_format, issues_enabled, pulls_enabled, next_iid, archived_at, \ | 15 | default_branch, object_format, issues_enabled, pulls_enabled, is_mirror, next_iid, \ |
| 16 | size_bytes, pushed_at, created_at, updated_at"; | 16 | archived_at, size_bytes, pushed_at, created_at, updated_at"; |
| 17 | 17 | ||
| 18 | /// Fields needed to create a repository. The server fills in the id, timestamps, | 18 | /// Fields needed to create a repository. The server fills in the id, timestamps, |
| 19 | /// and the derived size/push bookkeeping. | 19 | /// and the derived size/push bookkeeping. |
| @@ -69,6 +69,7 @@ impl Store { | |||
| 69 | object_format: "sha1".to_string(), | 69 | object_format: "sha1".to_string(), |
| 70 | issues_enabled: true, | 70 | issues_enabled: true, |
| 71 | pulls_enabled: true, | 71 | pulls_enabled: true, |
| 72 | is_mirror: false, | ||
| 72 | next_iid: 1, | 73 | next_iid: 1, |
| 73 | archived_at: None, | 74 | archived_at: None, |
| 74 | size_bytes: 0, | 75 | size_bytes: 0, |
| @@ -288,6 +289,23 @@ impl Store { | |||
| 288 | Ok(updated > 0) | 289 | Ok(updated > 0) |
| 289 | } | 290 | } |
| 290 | 291 | ||
| 292 | /// Mark a repository as a mirror (or not). Mirror repos are kept in sync from | ||
| 293 | /// a remote and have issues/PRs disabled. | ||
| 294 | /// | ||
| 295 | /// # Errors | ||
| 296 | /// | ||
| 297 | /// Returns [`StoreError::Query`] on a database failure. | ||
| 298 | pub async fn set_mirror(&self, repo_id: &str, is_mirror: bool) -> Result<bool, StoreError> { | ||
| 299 | let updated = sqlx::query("UPDATE repos SET is_mirror = $1, updated_at = $2 WHERE id = $3") | ||
| 300 | .bind(is_mirror) | ||
| 301 | .bind(now_ms()) | ||
| 302 | .bind(repo_id) | ||
| 303 | .execute(&self.pool) | ||
| 304 | .await? | ||
| 305 | .rows_affected(); | ||
| 306 | Ok(updated > 0) | ||
| 307 | } | ||
| 308 | |||
| 291 | /// Archive or unarchive a repository. Archiving stamps `archived_at`; | 309 | /// Archive or unarchive a repository. Archiving stamps `archived_at`; |
| 292 | /// unarchiving clears it. Returns `true` if the row existed and was updated. | 310 | /// unarchiving clears it. Returns `true` if the row existed and was updated. |
| 293 | /// | 311 | /// |
| @@ -448,6 +466,7 @@ impl Store { | |||
| 448 | object_format: row.try_get("object_format")?, | 466 | object_format: row.try_get("object_format")?, |
| 449 | issues_enabled: self.get_bool(row, "issues_enabled")?, | 467 | issues_enabled: self.get_bool(row, "issues_enabled")?, |
| 450 | pulls_enabled: self.get_bool(row, "pulls_enabled")?, | 468 | pulls_enabled: self.get_bool(row, "pulls_enabled")?, |
| 469 | is_mirror: self.get_bool(row, "is_mirror")?, | ||
| 451 | next_iid: row.try_get("next_iid")?, | 470 | next_iid: row.try_get("next_iid")?, |
| 452 | archived_at: row.try_get("archived_at")?, | 471 | archived_at: row.try_get("archived_at")?, |
| 453 | size_bytes: row.try_get("size_bytes")?, | 472 | size_bytes: row.try_get("size_bytes")?, |
crates/web/src/icons.rs +5 −0
| @@ -39,6 +39,7 @@ pub enum Icon { | |||
| 39 | Scale, | 39 | Scale, |
| 40 | Globe, | 40 | Globe, |
| 41 | Download, | 41 | Download, |
| 42 | Mirror, | ||
| 42 | Github, | 43 | Github, |
| 43 | Mastodon, | 44 | Mastodon, |
| 44 | } | 45 | } |
| @@ -118,6 +119,10 @@ impl Icon { | |||
| 118 | Icon::Download => { | 119 | Icon::Download => { |
| 119 | r#"<path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/>"# | 120 | r#"<path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/>"# |
| 120 | } | 121 | } |
| 122 | // Lucide "refresh-cw": circular arrows, for a mirror (kept in sync). | ||
| 123 | Icon::Mirror => { | ||
| 124 | r#"<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>"# | ||
| 125 | } | ||
| 121 | Icon::Github => { | 126 | Icon::Github => { |
| 122 | r#"<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>"# | 127 | r#"<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>"# |
| 123 | } | 128 | } |
crates/web/src/pages.rs +10 −0
| @@ -598,6 +598,16 @@ async fn do_migrate(state: &AppState, user: &User, form: &MigrateForm) -> Result | |||
| 598 | sync_on_push: false, | 598 | sync_on_push: false, |
| 599 | }) | 599 | }) |
| 600 | .await; | 600 | .await; |
| 601 | // A mirror is a read-only copy: mark it, and disable issues/PRs. | ||
| 602 | let _ = state.store.set_mirror(&repo.id, true).await; | ||
| 603 | let _ = state | ||
| 604 | .store | ||
| 605 | .set_feature_enabled(&repo.id, "issues", false) | ||
| 606 | .await; | ||
| 607 | let _ = state | ||
| 608 | .store | ||
| 609 | .set_feature_enabled(&repo.id, "pulls", false) | ||
| 610 | .await; | ||
| 601 | } | 611 | } |
| 602 | Ok(path) | 612 | Ok(path) |
| 603 | } | 613 | } |
crates/web/src/repo.rs +6 −1
| @@ -2354,6 +2354,8 @@ pub(crate) struct RepoEntry { | |||
| 2354 | pub(crate) updated: i64, | 2354 | pub(crate) updated: i64, |
| 2355 | /// Whether the repo is archived (shows an archived badge). | 2355 | /// Whether the repo is archived (shows an archived badge). |
| 2356 | pub(crate) archived: bool, | 2356 | pub(crate) archived: bool, |
| 2357 | /// Whether the repo is a mirror (shows the mirror icon). | ||
| 2358 | pub(crate) is_mirror: bool, | ||
| 2357 | /// Optional one-line description, shown under the name when present. | 2359 | /// Optional one-line description, shown under the name when present. |
| 2358 | pub(crate) description: Option<String>, | 2360 | pub(crate) description: Option<String>, |
| 2359 | } | 2361 | } |
| @@ -2368,6 +2370,7 @@ impl RepoEntry { | |||
| 2368 | default_branch: repo.default_branch.clone(), | 2370 | default_branch: repo.default_branch.clone(), |
| 2369 | updated: repo.pushed_at.unwrap_or(repo.updated_at), | 2371 | updated: repo.pushed_at.unwrap_or(repo.updated_at), |
| 2370 | archived: repo.archived_at.is_some(), | 2372 | archived: repo.archived_at.is_some(), |
| 2373 | is_mirror: repo.is_mirror, | ||
| 2371 | description: repo.description.clone(), | 2374 | description: repo.description.clone(), |
| 2372 | } | 2375 | } |
| 2373 | } | 2376 | } |
| @@ -2382,6 +2385,7 @@ impl RepoEntry { | |||
| 2382 | default_branch: repo.default_branch.clone(), | 2385 | default_branch: repo.default_branch.clone(), |
| 2383 | updated: repo.pushed_at.unwrap_or(repo.updated_at), | 2386 | updated: repo.pushed_at.unwrap_or(repo.updated_at), |
| 2384 | archived: repo.archived_at.is_some(), | 2387 | archived: repo.archived_at.is_some(), |
| 2388 | is_mirror: repo.is_mirror, | ||
| 2385 | description: repo.description.clone(), | 2389 | description: repo.description.clone(), |
| 2386 | } | 2390 | } |
| 2387 | } | 2391 | } |
| @@ -2421,7 +2425,8 @@ pub(crate) fn repo_card(title: &str, empty: &str, entries: &[RepoEntry]) -> Mark | |||
| 2421 | li { | 2425 | li { |
| 2422 | a class="repo-row" href=(e.href) { | 2426 | a class="repo-row" href=(e.href) { |
| 2423 | span class="repo-row-name" { | 2427 | span class="repo-row-name" { |
| 2424 | (icon(Icon::Box)) span { (e.label) } | 2428 | (icon(if e.is_mirror { Icon::Mirror } else { Icon::Box })) |
| 2429 | span { (e.label) } | ||
| 2425 | (visibility_badge(e.visibility)) | 2430 | (visibility_badge(e.visibility)) |
| 2426 | (archived_badge(e.archived)) | 2431 | (archived_badge(e.archived)) |
| 2427 | } | 2432 | } |
migrations/postgres/0015_repo_mirror.sql +1 −0
| @@ -0,0 +1 @@ | |||
| 1 | ALTER TABLE repos ADD COLUMN is_mirror BOOLEAN NOT NULL DEFAULT FALSE; | ||
migrations/sqlite/0015_repo_mirror.sql +1 −0
| @@ -0,0 +1 @@ | |||
| 1 | ALTER TABLE repos ADD COLUMN is_mirror INTEGER NOT NULL DEFAULT 0; | ||