FossilRepo
Delete _old_fossilrepo/, implement sync CLI commands Removed: - _old_fossilrepo/ directory (373 lines) — all code fully superseded: - server/config.py → Django Constance settings - server/manager.py → ctl/main.py repo commands + fossil/cli.py - sync/mirror.py → fossil/sync_models.py + fossil/tasks.py - sync/mappings.py → fossil/sync_models.SyncLog - cli/main.py → ctl/main.py Implemented: - `fossilrepo-ctl sync run <repo>` — runs Git sync for all configured mirrors - `fossilrepo-ctl sync status [repo]` — Rich table showing mirror status - Both use real models/tasks instead of NotImplementedError
Commit
89b91b106927708e7ca8735be051cfa7e91b27a0513e4a4e2af128c53eb0456a
Parent
d9f28de75dc31d1…
10 files changed
-1
-104
-66
-77
-39
-86
+63
-12
-
_old_fossilrepo/__init__.py
-
_old_fossilrepo/cli/__init__.py
-
_old_fossilrepo/cli/main.py
-
_old_fossilrepo/server/__init__.py
-
_old_fossilrepo/server/config.py
-
_old_fossilrepo/server/manager.py
-
_old_fossilrepo/sync/__init__.py
-
_old_fossilrepo/sync/mappings.py
-
_old_fossilrepo/sync/mirror.py
~
ctl/main.py
D
_old_fossilrepo/__init__.py
-1
| --- a/_old_fossilrepo/__init__.py | ||
| +++ b/_old_fossilrepo/__init__.py | ||
| @@ -1 +0,0 @@ | ||
| 1 | -__version__ = "0.1.0" |
| --- a/_old_fossilrepo/__init__.py | |
| +++ b/_old_fossilrepo/__init__.py | |
| @@ -1 +0,0 @@ | |
| 1 | __version__ = "0.1.0" |
| --- a/_old_fossilrepo/__init__.py | |
| +++ b/_old_fossilrepo/__init__.py | |
| @@ -1 +0,0 @@ | |
D
_old_fossilrepo/cli/__init__.py
No diff available
D
_old_fossilrepo/cli/main.py
-104
| --- a/_old_fossilrepo/cli/main.py | ||
| +++ b/_old_fossilrepo/cli/main.py | ||
| @@ -1,104 +0,0 @@ | ||
| 1 | -"""fossilrepo CLI — manage Fossil servers, repos, and Git sync.""" | |
| 2 | - | |
| 3 | -import click | |
| 4 | -from rich.console import Console | |
| 5 | - | |
| 6 | -console = Console() | |
| 7 | - | |
| 8 | - | |
| 9 | -@click.group() | |
| 10 | -@click.version_option(package_name="fossilrepo") | |
| 11 | -def cli() -> None: | |
| 12 | - """fossilrepo — self-hosted Fossil SCM infrastructure.""" | |
| 13 | - | |
| 14 | - | |
| 15 | -# --------------------------------------------------------------------------- | |
| 16 | -# Server commands | |
| 17 | -# --------------------------------------------------------------------------- | |
| 18 | - | |
| 19 | - | |
| 20 | -@cli.group() | |
| 21 | -def server() -> None: | |
| 22 | - """Manage the Fossil server.""" | |
| 23 | - | |
| 24 | - | |
| 25 | -@server.command() | |
| 26 | -def start() -> None: | |
| 27 | - """Start the Fossil server (Docker + Caddy + Litestream).""" | |
| 28 | - console.print("[bold]Starting Fossil server...[/bold]") | |
| 29 | - raise NotImplementedError | |
| 30 | - | |
| 31 | - | |
| 32 | -@server.command() | |
| 33 | -def stop() -> None: | |
| 34 | - """Stop the Fossil server.""" | |
| 35 | - console.print("[bold]Stopping Fossil server...[/bold]") | |
| 36 | - raise NotImplementedError | |
| 37 | - | |
| 38 | - | |
| 39 | -@server.command() | |
| 40 | -def status() -> None: | |
| 41 | - """Show Fossil server status.""" | |
| 42 | - console.print("[bold]Server status:[/bold]") | |
| 43 | - raise NotImplementedError | |
| 44 | - | |
| 45 | - | |
| 46 | -# --------------------------------------------------------------------------- | |
| 47 | -# Repo commands | |
| 48 | -# --------------------------------------------------------------------------- | |
| 49 | - | |
| 50 | - | |
| 51 | -@cli.group() | |
| 52 | -def repo() -> None: | |
| 53 | - """Manage Fossil repositories.""" | |
| 54 | - | |
| 55 | - | |
| 56 | -@repo.command() | |
| 57 | -@click.argument("name") | |
| 58 | -def create(name: str) -> None: | |
| 59 | - """Create a new Fossil repository.""" | |
| 60 | - console.print(f"[bold]Creating repo:[/bold] {name}") | |
| 61 | - raise NotImplementedError | |
| 62 | - | |
| 63 | - | |
| 64 | -@repo.command(name="list") | |
| 65 | -def list_repos() -> None: | |
| 66 | - """List all Fossil repositories.""" | |
| 67 | - raise NotImplementedError | |
| 68 | - | |
| 69 | - | |
| 70 | -@repo.command() | |
| 71 | -@click.argument("name") | |
| 72 | -def delete(name: str) -> None: | |
| 73 | - """Delete a Fossil repository.""" | |
| 74 | - console.print(f"[bold]Deleting repo:[/bold] {name}") | |
| 75 | - raise NotImplementedError | |
| 76 | - | |
| 77 | - | |
| 78 | -# --------------------------------------------------------------------------- | |
| 79 | -# Sync commands | |
| 80 | -# --------------------------------------------------------------------------- | |
| 81 | - | |
| 82 | - | |
| 83 | -@cli.group() | |
| 84 | -def sync() -> None: | |
| 85 | - """Sync Fossil repos to GitHub/GitLab.""" | |
| 86 | - | |
| 87 | - | |
| 88 | -@sync.command() | |
| 89 | -@click.argument("repo_name") | |
| 90 | -@click.option("--remote", required=True, help="Git remote URL to sync to.") | |
| 91 | -@click.option("--tickets/--no-tickets", default=False, help="Sync tickets as issues.") | |
| 92 | -@click.option("--wiki/--no-wiki", default=False, help="Sync wiki pages.") | |
| 93 | -def run(repo_name: str, remote: str, tickets: bool, wiki: bool) -> None: | |
| 94 | - """Run a sync from a Fossil repo to a Git remote.""" | |
| 95 | - console.print(f"[bold]Syncing[/bold] {repo_name} -> {remote}") | |
| 96 | - raise NotImplementedError | |
| 97 | - | |
| 98 | - | |
| 99 | -@sync.command() | |
| 100 | -@click.argument("repo_name") | |
| 101 | -def status(repo_name: str) -> None: # noqa: F811 | |
| 102 | - """Show sync status for a repository.""" | |
| 103 | - console.print(f"[bold]Sync status for:[/bold] {repo_name}") | |
| 104 | - raise NotImplementedError |
| --- a/_old_fossilrepo/cli/main.py | |
| +++ b/_old_fossilrepo/cli/main.py | |
| @@ -1,104 +0,0 @@ | |
| 1 | """fossilrepo CLI — manage Fossil servers, repos, and Git sync.""" |
| 2 | |
| 3 | import click |
| 4 | from rich.console import Console |
| 5 | |
| 6 | console = Console() |
| 7 | |
| 8 | |
| 9 | @click.group() |
| 10 | @click.version_option(package_name="fossilrepo") |
| 11 | def cli() -> None: |
| 12 | """fossilrepo — self-hosted Fossil SCM infrastructure.""" |
| 13 | |
| 14 | |
| 15 | # --------------------------------------------------------------------------- |
| 16 | # Server commands |
| 17 | # --------------------------------------------------------------------------- |
| 18 | |
| 19 | |
| 20 | @cli.group() |
| 21 | def server() -> None: |
| 22 | """Manage the Fossil server.""" |
| 23 | |
| 24 | |
| 25 | @server.command() |
| 26 | def start() -> None: |
| 27 | """Start the Fossil server (Docker + Caddy + Litestream).""" |
| 28 | console.print("[bold]Starting Fossil server...[/bold]") |
| 29 | raise NotImplementedError |
| 30 | |
| 31 | |
| 32 | @server.command() |
| 33 | def stop() -> None: |
| 34 | """Stop the Fossil server.""" |
| 35 | console.print("[bold]Stopping Fossil server...[/bold]") |
| 36 | raise NotImplementedError |
| 37 | |
| 38 | |
| 39 | @server.command() |
| 40 | def status() -> None: |
| 41 | """Show Fossil server status.""" |
| 42 | console.print("[bold]Server status:[/bold]") |
| 43 | raise NotImplementedError |
| 44 | |
| 45 | |
| 46 | # --------------------------------------------------------------------------- |
| 47 | # Repo commands |
| 48 | # --------------------------------------------------------------------------- |
| 49 | |
| 50 | |
| 51 | @cli.group() |
| 52 | def repo() -> None: |
| 53 | """Manage Fossil repositories.""" |
| 54 | |
| 55 | |
| 56 | @repo.command() |
| 57 | @click.argument("name") |
| 58 | def create(name: str) -> None: |
| 59 | """Create a new Fossil repository.""" |
| 60 | console.print(f"[bold]Creating repo:[/bold] {name}") |
| 61 | raise NotImplementedError |
| 62 | |
| 63 | |
| 64 | @repo.command(name="list") |
| 65 | def list_repos() -> None: |
| 66 | """List all Fossil repositories.""" |
| 67 | raise NotImplementedError |
| 68 | |
| 69 | |
| 70 | @repo.command() |
| 71 | @click.argument("name") |
| 72 | def delete(name: str) -> None: |
| 73 | """Delete a Fossil repository.""" |
| 74 | console.print(f"[bold]Deleting repo:[/bold] {name}") |
| 75 | raise NotImplementedError |
| 76 | |
| 77 | |
| 78 | # --------------------------------------------------------------------------- |
| 79 | # Sync commands |
| 80 | # --------------------------------------------------------------------------- |
| 81 | |
| 82 | |
| 83 | @cli.group() |
| 84 | def sync() -> None: |
| 85 | """Sync Fossil repos to GitHub/GitLab.""" |
| 86 | |
| 87 | |
| 88 | @sync.command() |
| 89 | @click.argument("repo_name") |
| 90 | @click.option("--remote", required=True, help="Git remote URL to sync to.") |
| 91 | @click.option("--tickets/--no-tickets", default=False, help="Sync tickets as issues.") |
| 92 | @click.option("--wiki/--no-wiki", default=False, help="Sync wiki pages.") |
| 93 | def run(repo_name: str, remote: str, tickets: bool, wiki: bool) -> None: |
| 94 | """Run a sync from a Fossil repo to a Git remote.""" |
| 95 | console.print(f"[bold]Syncing[/bold] {repo_name} -> {remote}") |
| 96 | raise NotImplementedError |
| 97 | |
| 98 | |
| 99 | @sync.command() |
| 100 | @click.argument("repo_name") |
| 101 | def status(repo_name: str) -> None: # noqa: F811 |
| 102 | """Show sync status for a repository.""" |
| 103 | console.print(f"[bold]Sync status for:[/bold] {repo_name}") |
| 104 | raise NotImplementedError |
| --- a/_old_fossilrepo/cli/main.py | |
| +++ b/_old_fossilrepo/cli/main.py | |
| @@ -1,104 +0,0 @@ | |
D
_old_fossilrepo/server/__init__.py
No diff available
D
_old_fossilrepo/server/config.py
-66
| --- a/_old_fossilrepo/server/config.py | ||
| +++ b/_old_fossilrepo/server/config.py | ||
| @@ -1,66 +0,0 @@ | ||
| 1 | -"""Server configuration for Fossil repository hosting.""" | |
| 2 | - | |
| 3 | -from pathlib import Path | |
| 4 | - | |
| 5 | -from pydantic import Field | |
| 6 | -from pydantic_settings import BaseSettings | |
| 7 | - | |
| 8 | - | |
| 9 | -class ServerConfig(BaseSettings): | |
| 10 | - """Configuration for the Fossil server infrastructure. | |
| 11 | - | |
| 12 | - Values are loaded from environment variables prefixed with FOSSILREPO_. | |
| 13 | - For example, FOSSILREPO_DATA_DIR sets data_dir. | |
| 14 | - """ | |
| 15 | - | |
| 16 | - model_config = {"env_prefix": "FOSSILREPO_"} | |
| 17 | - | |
| 18 | - data_dir: Path = Field( | |
| 19 | - default=Path("/data/repos"), | |
| 20 | - description="Directory where .fossil repository files are stored.", | |
| 21 | - ) | |
| 22 | - | |
| 23 | - caddy_domain: str = Field( | |
| 24 | - default="localhost", | |
| 25 | - description="Base domain for subdomain routing (e.g., fossilrepos.io).", | |
| 26 | - ) | |
| 27 | - | |
| 28 | - caddy_config_path: Path = Field( | |
| 29 | - default=Path("/etc/caddy/Caddyfile"), | |
| 30 | - description="Path to the Caddy configuration file.", | |
| 31 | - ) | |
| 32 | - | |
| 33 | - fossil_port: int = Field( | |
| 34 | - default=8080, | |
| 35 | - description="Port the fossil server listens on.", | |
| 36 | - ) | |
| 37 | - | |
| 38 | - s3_bucket: str = Field( | |
| 39 | - default="", | |
| 40 | - description="S3 bucket for Litestream replication.", | |
| 41 | - ) | |
| 42 | - | |
| 43 | - s3_endpoint: str = Field( | |
| 44 | - default="", | |
| 45 | - description="S3-compatible endpoint URL (for MinIO, R2, etc.).", | |
| 46 | - ) | |
| 47 | - | |
| 48 | - s3_access_key_id: str = Field( | |
| 49 | - default="", | |
| 50 | - description="AWS access key ID for S3 replication.", | |
| 51 | - ) | |
| 52 | - | |
| 53 | - s3_secret_access_key: str = Field( | |
| 54 | - default="", | |
| 55 | - description="AWS secret access key for S3 replication.", | |
| 56 | - ) | |
| 57 | - | |
| 58 | - s3_region: str = Field( | |
| 59 | - default="us-east-1", | |
| 60 | - description="AWS region for S3 bucket.", | |
| 61 | - ) | |
| 62 | - | |
| 63 | - litestream_config_path: Path = Field( | |
| 64 | - default=Path("/etc/litestream.yml"), | |
| 65 | - description="Path to the Litestream configuration file.", | |
| 66 | - ) |
| --- a/_old_fossilrepo/server/config.py | |
| +++ b/_old_fossilrepo/server/config.py | |
| @@ -1,66 +0,0 @@ | |
| 1 | """Server configuration for Fossil repository hosting.""" |
| 2 | |
| 3 | from pathlib import Path |
| 4 | |
| 5 | from pydantic import Field |
| 6 | from pydantic_settings import BaseSettings |
| 7 | |
| 8 | |
| 9 | class ServerConfig(BaseSettings): |
| 10 | """Configuration for the Fossil server infrastructure. |
| 11 | |
| 12 | Values are loaded from environment variables prefixed with FOSSILREPO_. |
| 13 | For example, FOSSILREPO_DATA_DIR sets data_dir. |
| 14 | """ |
| 15 | |
| 16 | model_config = {"env_prefix": "FOSSILREPO_"} |
| 17 | |
| 18 | data_dir: Path = Field( |
| 19 | default=Path("/data/repos"), |
| 20 | description="Directory where .fossil repository files are stored.", |
| 21 | ) |
| 22 | |
| 23 | caddy_domain: str = Field( |
| 24 | default="localhost", |
| 25 | description="Base domain for subdomain routing (e.g., fossilrepos.io).", |
| 26 | ) |
| 27 | |
| 28 | caddy_config_path: Path = Field( |
| 29 | default=Path("/etc/caddy/Caddyfile"), |
| 30 | description="Path to the Caddy configuration file.", |
| 31 | ) |
| 32 | |
| 33 | fossil_port: int = Field( |
| 34 | default=8080, |
| 35 | description="Port the fossil server listens on.", |
| 36 | ) |
| 37 | |
| 38 | s3_bucket: str = Field( |
| 39 | default="", |
| 40 | description="S3 bucket for Litestream replication.", |
| 41 | ) |
| 42 | |
| 43 | s3_endpoint: str = Field( |
| 44 | default="", |
| 45 | description="S3-compatible endpoint URL (for MinIO, R2, etc.).", |
| 46 | ) |
| 47 | |
| 48 | s3_access_key_id: str = Field( |
| 49 | default="", |
| 50 | description="AWS access key ID for S3 replication.", |
| 51 | ) |
| 52 | |
| 53 | s3_secret_access_key: str = Field( |
| 54 | default="", |
| 55 | description="AWS secret access key for S3 replication.", |
| 56 | ) |
| 57 | |
| 58 | s3_region: str = Field( |
| 59 | default="us-east-1", |
| 60 | description="AWS region for S3 bucket.", |
| 61 | ) |
| 62 | |
| 63 | litestream_config_path: Path = Field( |
| 64 | default=Path("/etc/litestream.yml"), |
| 65 | description="Path to the Litestream configuration file.", |
| 66 | ) |
| --- a/_old_fossilrepo/server/config.py | |
| +++ b/_old_fossilrepo/server/config.py | |
| @@ -1,66 +0,0 @@ | |
D
_old_fossilrepo/server/manager.py
-77
| --- a/_old_fossilrepo/server/manager.py | ||
| +++ b/_old_fossilrepo/server/manager.py | ||
| @@ -1,77 +0,0 @@ | ||
| 1 | -"""Fossil repository management — create, delete, list, inspect repos.""" | |
| 2 | - | |
| 3 | -from pathlib import Path | |
| 4 | - | |
| 5 | -from fossilrepo.server.config import ServerConfig | |
| 6 | - | |
| 7 | - | |
| 8 | -class RepoInfo: | |
| 9 | - """Information about a single Fossil repository.""" | |
| 10 | - | |
| 11 | - def __init__(self, name: str, path: Path, size_bytes: int) -> None: | |
| 12 | - self.name = name | |
| 13 | - self.path = path | |
| 14 | - self.size_bytes = size_bytes | |
| 15 | - | |
| 16 | - | |
| 17 | -class FossilRepoManager: | |
| 18 | - """Manages Fossil repositories on the server. | |
| 19 | - | |
| 20 | - Handles repo lifecycle: creation via `fossil init`, deletion (soft — moves | |
| 21 | - to trash), listing, and metadata inspection. Coordinates with Litestream | |
| 22 | - for S3 replication of new repos. | |
| 23 | - """ | |
| 24 | - | |
| 25 | - def __init__(self, config: ServerConfig | None = None) -> None: | |
| 26 | - self.config = config or ServerConfig() | |
| 27 | - | |
| 28 | - def create_repo(self, name: str) -> RepoInfo: | |
| 29 | - """Create a new Fossil repository. | |
| 30 | - | |
| 31 | - Runs `fossil init` to create the .fossil file in the data directory, | |
| 32 | - registers the repo with Caddy for subdomain routing, and ensures | |
| 33 | - Litestream picks up the new file for replication. | |
| 34 | - | |
| 35 | - Args: | |
| 36 | - name: Repository name. Used as the subdomain and filename. | |
| 37 | - | |
| 38 | - Returns: | |
| 39 | - RepoInfo for the newly created repository. | |
| 40 | - """ | |
| 41 | - raise NotImplementedError | |
| 42 | - | |
| 43 | - def delete_repo(self, name: str) -> None: | |
| 44 | - """Soft-delete a Fossil repository. | |
| 45 | - | |
| 46 | - Moves the .fossil file to a trash directory rather than deleting it. | |
| 47 | - Removes the Caddy subdomain route. Litestream retains the S3 replica. | |
| 48 | - | |
| 49 | - Args: | |
| 50 | - name: Repository name to delete. | |
| 51 | - """ | |
| 52 | - raise NotImplementedError | |
| 53 | - | |
| 54 | - def list_repos(self) -> list[RepoInfo]: | |
| 55 | - """List all active Fossil repositories. | |
| 56 | - | |
| 57 | - Scans the data directory for .fossil files and returns metadata | |
| 58 | - for each. | |
| 59 | - | |
| 60 | - Returns: | |
| 61 | - List of RepoInfo objects for all active repositories. | |
| 62 | - """ | |
| 63 | - raise NotImplementedError | |
| 64 | - | |
| 65 | - def get_repo_info(self, name: str) -> RepoInfo: | |
| 66 | - """Get detailed information about a specific repository. | |
| 67 | - | |
| 68 | - Args: | |
| 69 | - name: Repository name to inspect. | |
| 70 | - | |
| 71 | - Returns: | |
| 72 | - RepoInfo with metadata about the repository. | |
| 73 | - | |
| 74 | - Raises: | |
| 75 | - FileNotFoundError: If the repository does not exist. | |
| 76 | - """ | |
| 77 | - raise NotImplementedError |
| --- a/_old_fossilrepo/server/manager.py | |
| +++ b/_old_fossilrepo/server/manager.py | |
| @@ -1,77 +0,0 @@ | |
| 1 | """Fossil repository management — create, delete, list, inspect repos.""" |
| 2 | |
| 3 | from pathlib import Path |
| 4 | |
| 5 | from fossilrepo.server.config import ServerConfig |
| 6 | |
| 7 | |
| 8 | class RepoInfo: |
| 9 | """Information about a single Fossil repository.""" |
| 10 | |
| 11 | def __init__(self, name: str, path: Path, size_bytes: int) -> None: |
| 12 | self.name = name |
| 13 | self.path = path |
| 14 | self.size_bytes = size_bytes |
| 15 | |
| 16 | |
| 17 | class FossilRepoManager: |
| 18 | """Manages Fossil repositories on the server. |
| 19 | |
| 20 | Handles repo lifecycle: creation via `fossil init`, deletion (soft — moves |
| 21 | to trash), listing, and metadata inspection. Coordinates with Litestream |
| 22 | for S3 replication of new repos. |
| 23 | """ |
| 24 | |
| 25 | def __init__(self, config: ServerConfig | None = None) -> None: |
| 26 | self.config = config or ServerConfig() |
| 27 | |
| 28 | def create_repo(self, name: str) -> RepoInfo: |
| 29 | """Create a new Fossil repository. |
| 30 | |
| 31 | Runs `fossil init` to create the .fossil file in the data directory, |
| 32 | registers the repo with Caddy for subdomain routing, and ensures |
| 33 | Litestream picks up the new file for replication. |
| 34 | |
| 35 | Args: |
| 36 | name: Repository name. Used as the subdomain and filename. |
| 37 | |
| 38 | Returns: |
| 39 | RepoInfo for the newly created repository. |
| 40 | """ |
| 41 | raise NotImplementedError |
| 42 | |
| 43 | def delete_repo(self, name: str) -> None: |
| 44 | """Soft-delete a Fossil repository. |
| 45 | |
| 46 | Moves the .fossil file to a trash directory rather than deleting it. |
| 47 | Removes the Caddy subdomain route. Litestream retains the S3 replica. |
| 48 | |
| 49 | Args: |
| 50 | name: Repository name to delete. |
| 51 | """ |
| 52 | raise NotImplementedError |
| 53 | |
| 54 | def list_repos(self) -> list[RepoInfo]: |
| 55 | """List all active Fossil repositories. |
| 56 | |
| 57 | Scans the data directory for .fossil files and returns metadata |
| 58 | for each. |
| 59 | |
| 60 | Returns: |
| 61 | List of RepoInfo objects for all active repositories. |
| 62 | """ |
| 63 | raise NotImplementedError |
| 64 | |
| 65 | def get_repo_info(self, name: str) -> RepoInfo: |
| 66 | """Get detailed information about a specific repository. |
| 67 | |
| 68 | Args: |
| 69 | name: Repository name to inspect. |
| 70 | |
| 71 | Returns: |
| 72 | RepoInfo with metadata about the repository. |
| 73 | |
| 74 | Raises: |
| 75 | FileNotFoundError: If the repository does not exist. |
| 76 | """ |
| 77 | raise NotImplementedError |
| --- a/_old_fossilrepo/server/manager.py | |
| +++ b/_old_fossilrepo/server/manager.py | |
| @@ -1,77 +0,0 @@ | |
D
_old_fossilrepo/sync/__init__.py
No diff available
D
_old_fossilrepo/sync/mappings.py
-39
| --- a/_old_fossilrepo/sync/mappings.py | ||
| +++ b/_old_fossilrepo/sync/mappings.py | ||
| @@ -1,39 +0,0 @@ | ||
| 1 | -"""Data models for Fossil-to-Git sync mappings.""" | |
| 2 | - | |
| 3 | -from datetime import datetime | |
| 4 | - | |
| 5 | -from pydantic import BaseModel, Field | |
| 6 | - | |
| 7 | - | |
| 8 | -class CommitMapping(BaseModel): | |
| 9 | - """Maps a Fossil checkin to a Git commit.""" | |
| 10 | - | |
| 11 | - fossil_hash: str = Field(description="Fossil checkin hash (SHA1).") | |
| 12 | - git_sha: str = Field(description="Corresponding Git commit SHA.") | |
| 13 | - timestamp: datetime = Field(description="Commit timestamp.") | |
| 14 | - message: str = Field(description="Commit message.") | |
| 15 | - author: str = Field(description="Author name.") | |
| 16 | - | |
| 17 | - | |
| 18 | -class TicketMapping(BaseModel): | |
| 19 | - """Maps a Fossil ticket to a GitHub/GitLab issue.""" | |
| 20 | - | |
| 21 | - fossil_ticket_id: str = Field(description="Fossil ticket UUID.") | |
| 22 | - remote_issue_number: int = Field(description="GitHub/GitLab issue number.") | |
| 23 | - remote_issue_url: str = Field(description="URL to the remote issue.") | |
| 24 | - title: str = Field(description="Ticket/issue title.") | |
| 25 | - status: str = Field(description="Current status (open, closed, etc.).") | |
| 26 | - last_synced: datetime = Field(description="Timestamp of last sync.") | |
| 27 | - | |
| 28 | - | |
| 29 | -class WikiMapping(BaseModel): | |
| 30 | - """Maps a Fossil wiki page to a remote doc/wiki page.""" | |
| 31 | - | |
| 32 | - fossil_page_name: str = Field(description="Fossil wiki page name.") | |
| 33 | - remote_path: str = Field( | |
| 34 | - description="Path in the remote repo (e.g., docs/page.md) or wiki URL." | |
| 35 | - ) | |
| 36 | - last_synced: datetime = Field(description="Timestamp of last sync.") | |
| 37 | - content_hash: str = Field( | |
| 38 | - description="Hash of the content at last sync, for change detection." | |
| 39 | - ) |
| --- a/_old_fossilrepo/sync/mappings.py | |
| +++ b/_old_fossilrepo/sync/mappings.py | |
| @@ -1,39 +0,0 @@ | |
| 1 | """Data models for Fossil-to-Git sync mappings.""" |
| 2 | |
| 3 | from datetime import datetime |
| 4 | |
| 5 | from pydantic import BaseModel, Field |
| 6 | |
| 7 | |
| 8 | class CommitMapping(BaseModel): |
| 9 | """Maps a Fossil checkin to a Git commit.""" |
| 10 | |
| 11 | fossil_hash: str = Field(description="Fossil checkin hash (SHA1).") |
| 12 | git_sha: str = Field(description="Corresponding Git commit SHA.") |
| 13 | timestamp: datetime = Field(description="Commit timestamp.") |
| 14 | message: str = Field(description="Commit message.") |
| 15 | author: str = Field(description="Author name.") |
| 16 | |
| 17 | |
| 18 | class TicketMapping(BaseModel): |
| 19 | """Maps a Fossil ticket to a GitHub/GitLab issue.""" |
| 20 | |
| 21 | fossil_ticket_id: str = Field(description="Fossil ticket UUID.") |
| 22 | remote_issue_number: int = Field(description="GitHub/GitLab issue number.") |
| 23 | remote_issue_url: str = Field(description="URL to the remote issue.") |
| 24 | title: str = Field(description="Ticket/issue title.") |
| 25 | status: str = Field(description="Current status (open, closed, etc.).") |
| 26 | last_synced: datetime = Field(description="Timestamp of last sync.") |
| 27 | |
| 28 | |
| 29 | class WikiMapping(BaseModel): |
| 30 | """Maps a Fossil wiki page to a remote doc/wiki page.""" |
| 31 | |
| 32 | fossil_page_name: str = Field(description="Fossil wiki page name.") |
| 33 | remote_path: str = Field( |
| 34 | description="Path in the remote repo (e.g., docs/page.md) or wiki URL." |
| 35 | ) |
| 36 | last_synced: datetime = Field(description="Timestamp of last sync.") |
| 37 | content_hash: str = Field( |
| 38 | description="Hash of the content at last sync, for change detection." |
| 39 | ) |
| --- a/_old_fossilrepo/sync/mappings.py | |
| +++ b/_old_fossilrepo/sync/mappings.py | |
| @@ -1,39 +0,0 @@ | |
D
_old_fossilrepo/sync/mirror.py
-86
| --- a/_old_fossilrepo/sync/mirror.py | ||
| +++ b/_old_fossilrepo/sync/mirror.py | ||
| @@ -1,86 +0,0 @@ | ||
| 1 | -"""Fossil-to-Git mirror — sync commits, tickets, and wiki to GitHub/GitLab.""" | |
| 2 | - | |
| 3 | -from pathlib import Path | |
| 4 | - | |
| 5 | -from fossilrepo.sync.mappings import CommitMapping, TicketMapping, WikiMapping | |
| 6 | - | |
| 7 | - | |
| 8 | -class FossilMirror: | |
| 9 | - """Mirrors a Fossil repository to a Git remote (GitHub or GitLab). | |
| 10 | - | |
| 11 | - Fossil is the source of truth. The Git remote is a downstream mirror | |
| 12 | - for ecosystem visibility. Syncs commits, optionally maps tickets to | |
| 13 | - issues and wiki pages to docs. | |
| 14 | - """ | |
| 15 | - | |
| 16 | - def __init__(self, fossil_path: Path, remote_url: str) -> None: | |
| 17 | - self.fossil_path = fossil_path | |
| 18 | - self.remote_url = remote_url | |
| 19 | - | |
| 20 | - def sync_to_github( | |
| 21 | - self, | |
| 22 | - *, | |
| 23 | - include_tickets: bool = False, | |
| 24 | - include_wiki: bool = False, | |
| 25 | - ) -> None: | |
| 26 | - """Run a full sync to a GitHub repository. | |
| 27 | - | |
| 28 | - Exports Fossil commits to Git format and pushes to the GitHub remote. | |
| 29 | - Optionally syncs tickets as GitHub Issues and wiki as repo docs. | |
| 30 | - | |
| 31 | - Args: | |
| 32 | - include_tickets: If True, map Fossil tickets to GitHub Issues. | |
| 33 | - include_wiki: If True, export Fossil wiki pages to repo docs. | |
| 34 | - """ | |
| 35 | - raise NotImplementedError | |
| 36 | - | |
| 37 | - def sync_to_gitlab( | |
| 38 | - self, | |
| 39 | - *, | |
| 40 | - include_tickets: bool = False, | |
| 41 | - include_wiki: bool = False, | |
| 42 | - ) -> None: | |
| 43 | - """Run a full sync to a GitLab repository. | |
| 44 | - | |
| 45 | - Exports Fossil commits to Git format and pushes to the GitLab remote. | |
| 46 | - Optionally syncs tickets as GitLab Issues and wiki pages. | |
| 47 | - | |
| 48 | - Args: | |
| 49 | - include_tickets: If True, map Fossil tickets to GitLab Issues. | |
| 50 | - include_wiki: If True, export Fossil wiki pages to GitLab wiki. | |
| 51 | - """ | |
| 52 | - raise NotImplementedError | |
| 53 | - | |
| 54 | - def sync_commits(self) -> list[CommitMapping]: | |
| 55 | - """Sync Fossil commits to the Git remote. | |
| 56 | - | |
| 57 | - Exports the Fossil timeline as Git commits and pushes to the | |
| 58 | - configured remote. Returns a mapping of Fossil checkin hashes | |
| 59 | - to Git commit SHAs. | |
| 60 | - | |
| 61 | - Returns: | |
| 62 | - List of CommitMapping objects for each synced commit. | |
| 63 | - """ | |
| 64 | - raise NotImplementedError | |
| 65 | - | |
| 66 | - def sync_tickets(self) -> list[TicketMapping]: | |
| 67 | - """Sync Fossil tickets to the remote issue tracker. | |
| 68 | - | |
| 69 | - Maps Fossil ticket fields to GitHub/GitLab issue fields. Creates | |
| 70 | - new issues for new tickets, updates existing ones. | |
| 71 | - | |
| 72 | - Returns: | |
| 73 | - List of TicketMapping objects for each synced ticket. | |
| 74 | - """ | |
| 75 | - raise NotImplementedError | |
| 76 | - | |
| 77 | - def sync_wiki(self) -> list[WikiMapping]: | |
| 78 | - """Sync Fossil wiki pages to the remote. | |
| 79 | - | |
| 80 | - Exports Fossil wiki pages as Markdown files. For GitHub, these go | |
| 81 | - into a docs/ directory. For GitLab, they go to the project wiki. | |
| 82 | - | |
| 83 | - Returns: | |
| 84 | - List of WikiMapping objects for each synced page. | |
| 85 | - """ | |
| 86 | - raise NotImplementedError |
| --- a/_old_fossilrepo/sync/mirror.py | |
| +++ b/_old_fossilrepo/sync/mirror.py | |
| @@ -1,86 +0,0 @@ | |
| 1 | """Fossil-to-Git mirror — sync commits, tickets, and wiki to GitHub/GitLab.""" |
| 2 | |
| 3 | from pathlib import Path |
| 4 | |
| 5 | from fossilrepo.sync.mappings import CommitMapping, TicketMapping, WikiMapping |
| 6 | |
| 7 | |
| 8 | class FossilMirror: |
| 9 | """Mirrors a Fossil repository to a Git remote (GitHub or GitLab). |
| 10 | |
| 11 | Fossil is the source of truth. The Git remote is a downstream mirror |
| 12 | for ecosystem visibility. Syncs commits, optionally maps tickets to |
| 13 | issues and wiki pages to docs. |
| 14 | """ |
| 15 | |
| 16 | def __init__(self, fossil_path: Path, remote_url: str) -> None: |
| 17 | self.fossil_path = fossil_path |
| 18 | self.remote_url = remote_url |
| 19 | |
| 20 | def sync_to_github( |
| 21 | self, |
| 22 | *, |
| 23 | include_tickets: bool = False, |
| 24 | include_wiki: bool = False, |
| 25 | ) -> None: |
| 26 | """Run a full sync to a GitHub repository. |
| 27 | |
| 28 | Exports Fossil commits to Git format and pushes to the GitHub remote. |
| 29 | Optionally syncs tickets as GitHub Issues and wiki as repo docs. |
| 30 | |
| 31 | Args: |
| 32 | include_tickets: If True, map Fossil tickets to GitHub Issues. |
| 33 | include_wiki: If True, export Fossil wiki pages to repo docs. |
| 34 | """ |
| 35 | raise NotImplementedError |
| 36 | |
| 37 | def sync_to_gitlab( |
| 38 | self, |
| 39 | *, |
| 40 | include_tickets: bool = False, |
| 41 | include_wiki: bool = False, |
| 42 | ) -> None: |
| 43 | """Run a full sync to a GitLab repository. |
| 44 | |
| 45 | Exports Fossil commits to Git format and pushes to the GitLab remote. |
| 46 | Optionally syncs tickets as GitLab Issues and wiki pages. |
| 47 | |
| 48 | Args: |
| 49 | include_tickets: If True, map Fossil tickets to GitLab Issues. |
| 50 | include_wiki: If True, export Fossil wiki pages to GitLab wiki. |
| 51 | """ |
| 52 | raise NotImplementedError |
| 53 | |
| 54 | def sync_commits(self) -> list[CommitMapping]: |
| 55 | """Sync Fossil commits to the Git remote. |
| 56 | |
| 57 | Exports the Fossil timeline as Git commits and pushes to the |
| 58 | configured remote. Returns a mapping of Fossil checkin hashes |
| 59 | to Git commit SHAs. |
| 60 | |
| 61 | Returns: |
| 62 | List of CommitMapping objects for each synced commit. |
| 63 | """ |
| 64 | raise NotImplementedError |
| 65 | |
| 66 | def sync_tickets(self) -> list[TicketMapping]: |
| 67 | """Sync Fossil tickets to the remote issue tracker. |
| 68 | |
| 69 | Maps Fossil ticket fields to GitHub/GitLab issue fields. Creates |
| 70 | new issues for new tickets, updates existing ones. |
| 71 | |
| 72 | Returns: |
| 73 | List of TicketMapping objects for each synced ticket. |
| 74 | """ |
| 75 | raise NotImplementedError |
| 76 | |
| 77 | def sync_wiki(self) -> list[WikiMapping]: |
| 78 | """Sync Fossil wiki pages to the remote. |
| 79 | |
| 80 | Exports Fossil wiki pages as Markdown files. For GitHub, these go |
| 81 | into a docs/ directory. For GitLab, they go to the project wiki. |
| 82 | |
| 83 | Returns: |
| 84 | List of WikiMapping objects for each synced page. |
| 85 | """ |
| 86 | raise NotImplementedError |
| --- a/_old_fossilrepo/sync/mirror.py | |
| +++ b/_old_fossilrepo/sync/mirror.py | |
| @@ -1,86 +0,0 @@ | |
+63
-12
| --- ctl/main.py | ||
| +++ ctl/main.py | ||
| @@ -206,25 +206,76 @@ | ||
| 206 | 206 | """Sync Fossil repos to GitHub/GitLab.""" |
| 207 | 207 | |
| 208 | 208 | |
| 209 | 209 | @sync.command(name="run") |
| 210 | 210 | @click.argument("repo_name") |
| 211 | -@click.option("--remote", required=True, help="Git remote URL.") | |
| 212 | -@click.option("--tickets/--no-tickets", default=False, help="Sync tickets as issues.") | |
| 213 | -@click.option("--wiki/--no-wiki", default=False, help="Sync wiki pages.") | |
| 214 | -def sync_run(repo_name: str, remote: str, tickets: bool, wiki: bool) -> None: | |
| 215 | - """Run a sync from a Fossil repo to a Git remote.""" | |
| 216 | - console.print(f"[bold]Syncing[/bold] {repo_name} -> {remote}") | |
| 217 | - raise NotImplementedError("Sync not yet implemented") | |
| 211 | +@click.option("--mirror-id", type=int, help="Specific Git mirror ID to sync.") | |
| 212 | +def sync_run(repo_name: str, mirror_id: int | None = None) -> None: | |
| 213 | + """Run Git sync for a Fossil repository.""" | |
| 214 | + import django | |
| 215 | + | |
| 216 | + django.setup() | |
| 217 | + from fossil.models import FossilRepository | |
| 218 | + from fossil.sync_models import GitMirror | |
| 219 | + from fossil.tasks import run_git_sync | |
| 220 | + | |
| 221 | + repo = FossilRepository.objects.filter(filename=f"{repo_name}.fossil").first() | |
| 222 | + if not repo: | |
| 223 | + console.print(f"[red]Repo not found: {repo_name}.fossil[/red]") | |
| 224 | + return | |
| 225 | + | |
| 226 | + mirrors = GitMirror.objects.filter(repository=repo, deleted_at__isnull=True).exclude(sync_mode="disabled") | |
| 227 | + if mirror_id: | |
| 228 | + mirrors = mirrors.filter(pk=mirror_id) | |
| 229 | + | |
| 230 | + if not mirrors.exists(): | |
| 231 | + console.print("[yellow]No Git mirrors configured for this repo.[/yellow]") | |
| 232 | + return | |
| 233 | + | |
| 234 | + for mirror in mirrors: | |
| 235 | + console.print(f"[bold]Syncing[/bold] {repo.filename} → {mirror.git_remote_url}") | |
| 236 | + run_git_sync(mirror.pk) | |
| 237 | + mirror.refresh_from_db() | |
| 238 | + if mirror.last_sync_status == "success": | |
| 239 | + console.print(f" [green]Success[/green] — {mirror.last_sync_message[:100]}") | |
| 240 | + else: | |
| 241 | + console.print(f" [red]Failed[/red] — {mirror.last_sync_message[:100]}") | |
| 218 | 242 | |
| 219 | 243 | |
| 220 | 244 | @sync.command(name="status") |
| 221 | -@click.argument("repo_name") | |
| 222 | -def sync_status(repo_name: str) -> None: | |
| 223 | - """Show sync status for a repository.""" | |
| 224 | - console.print(f"[bold]Sync status for:[/bold] {repo_name}") | |
| 225 | - raise NotImplementedError("Sync status not yet implemented") | |
| 245 | +@click.argument("repo_name", required=False) | |
| 246 | +def sync_status(repo_name: str | None = None) -> None: | |
| 247 | + """Show sync status for repositories.""" | |
| 248 | + import django | |
| 249 | + | |
| 250 | + django.setup() | |
| 251 | + from rich.table import Table | |
| 252 | + | |
| 253 | + from fossil.sync_models import GitMirror | |
| 254 | + | |
| 255 | + mirrors = GitMirror.objects.filter(deleted_at__isnull=True) | |
| 256 | + if repo_name: | |
| 257 | + mirrors = mirrors.filter(repository__filename=f"{repo_name}.fossil") | |
| 258 | + | |
| 259 | + table = Table(title="Git Mirror Status") | |
| 260 | + table.add_column("Repo", style="cyan") | |
| 261 | + table.add_column("Remote") | |
| 262 | + table.add_column("Mode") | |
| 263 | + table.add_column("Status") | |
| 264 | + table.add_column("Last Sync") | |
| 265 | + table.add_column("Syncs", justify="right") | |
| 266 | + for m in mirrors: | |
| 267 | + status_style = "green" if m.last_sync_status == "success" else "red" if m.last_sync_status == "failed" else "yellow" | |
| 268 | + table.add_row( | |
| 269 | + m.repository.filename, | |
| 270 | + m.git_remote_url[:40], | |
| 271 | + m.get_sync_mode_display(), | |
| 272 | + f"[{status_style}]{m.last_sync_status or 'never'}[/{status_style}]", | |
| 273 | + str(m.last_sync_at.strftime("%Y-%m-%d %H:%M") if m.last_sync_at else "—"), | |
| 274 | + str(m.total_syncs), | |
| 275 | + ) | |
| 276 | + console.print(table) | |
| 226 | 277 | |
| 227 | 278 | |
| 228 | 279 | # --------------------------------------------------------------------------- |
| 229 | 280 | # Backup commands |
| 230 | 281 | # --------------------------------------------------------------------------- |
| 231 | 282 |
| --- ctl/main.py | |
| +++ ctl/main.py | |
| @@ -206,25 +206,76 @@ | |
| 206 | """Sync Fossil repos to GitHub/GitLab.""" |
| 207 | |
| 208 | |
| 209 | @sync.command(name="run") |
| 210 | @click.argument("repo_name") |
| 211 | @click.option("--remote", required=True, help="Git remote URL.") |
| 212 | @click.option("--tickets/--no-tickets", default=False, help="Sync tickets as issues.") |
| 213 | @click.option("--wiki/--no-wiki", default=False, help="Sync wiki pages.") |
| 214 | def sync_run(repo_name: str, remote: str, tickets: bool, wiki: bool) -> None: |
| 215 | """Run a sync from a Fossil repo to a Git remote.""" |
| 216 | console.print(f"[bold]Syncing[/bold] {repo_name} -> {remote}") |
| 217 | raise NotImplementedError("Sync not yet implemented") |
| 218 | |
| 219 | |
| 220 | @sync.command(name="status") |
| 221 | @click.argument("repo_name") |
| 222 | def sync_status(repo_name: str) -> None: |
| 223 | """Show sync status for a repository.""" |
| 224 | console.print(f"[bold]Sync status for:[/bold] {repo_name}") |
| 225 | raise NotImplementedError("Sync status not yet implemented") |
| 226 | |
| 227 | |
| 228 | # --------------------------------------------------------------------------- |
| 229 | # Backup commands |
| 230 | # --------------------------------------------------------------------------- |
| 231 |
| --- ctl/main.py | |
| +++ ctl/main.py | |
| @@ -206,25 +206,76 @@ | |
| 206 | """Sync Fossil repos to GitHub/GitLab.""" |
| 207 | |
| 208 | |
| 209 | @sync.command(name="run") |
| 210 | @click.argument("repo_name") |
| 211 | @click.option("--mirror-id", type=int, help="Specific Git mirror ID to sync.") |
| 212 | def sync_run(repo_name: str, mirror_id: int | None = None) -> None: |
| 213 | """Run Git sync for a Fossil repository.""" |
| 214 | import django |
| 215 | |
| 216 | django.setup() |
| 217 | from fossil.models import FossilRepository |
| 218 | from fossil.sync_models import GitMirror |
| 219 | from fossil.tasks import run_git_sync |
| 220 | |
| 221 | repo = FossilRepository.objects.filter(filename=f"{repo_name}.fossil").first() |
| 222 | if not repo: |
| 223 | console.print(f"[red]Repo not found: {repo_name}.fossil[/red]") |
| 224 | return |
| 225 | |
| 226 | mirrors = GitMirror.objects.filter(repository=repo, deleted_at__isnull=True).exclude(sync_mode="disabled") |
| 227 | if mirror_id: |
| 228 | mirrors = mirrors.filter(pk=mirror_id) |
| 229 | |
| 230 | if not mirrors.exists(): |
| 231 | console.print("[yellow]No Git mirrors configured for this repo.[/yellow]") |
| 232 | return |
| 233 | |
| 234 | for mirror in mirrors: |
| 235 | console.print(f"[bold]Syncing[/bold] {repo.filename} → {mirror.git_remote_url}") |
| 236 | run_git_sync(mirror.pk) |
| 237 | mirror.refresh_from_db() |
| 238 | if mirror.last_sync_status == "success": |
| 239 | console.print(f" [green]Success[/green] — {mirror.last_sync_message[:100]}") |
| 240 | else: |
| 241 | console.print(f" [red]Failed[/red] — {mirror.last_sync_message[:100]}") |
| 242 | |
| 243 | |
| 244 | @sync.command(name="status") |
| 245 | @click.argument("repo_name", required=False) |
| 246 | def sync_status(repo_name: str | None = None) -> None: |
| 247 | """Show sync status for repositories.""" |
| 248 | import django |
| 249 | |
| 250 | django.setup() |
| 251 | from rich.table import Table |
| 252 | |
| 253 | from fossil.sync_models import GitMirror |
| 254 | |
| 255 | mirrors = GitMirror.objects.filter(deleted_at__isnull=True) |
| 256 | if repo_name: |
| 257 | mirrors = mirrors.filter(repository__filename=f"{repo_name}.fossil") |
| 258 | |
| 259 | table = Table(title="Git Mirror Status") |
| 260 | table.add_column("Repo", style="cyan") |
| 261 | table.add_column("Remote") |
| 262 | table.add_column("Mode") |
| 263 | table.add_column("Status") |
| 264 | table.add_column("Last Sync") |
| 265 | table.add_column("Syncs", justify="right") |
| 266 | for m in mirrors: |
| 267 | status_style = "green" if m.last_sync_status == "success" else "red" if m.last_sync_status == "failed" else "yellow" |
| 268 | table.add_row( |
| 269 | m.repository.filename, |
| 270 | m.git_remote_url[:40], |
| 271 | m.get_sync_mode_display(), |
| 272 | f"[{status_style}]{m.last_sync_status or 'never'}[/{status_style}]", |
| 273 | str(m.last_sync_at.strftime("%Y-%m-%d %H:%M") if m.last_sync_at else "—"), |
| 274 | str(m.total_syncs), |
| 275 | ) |
| 276 | console.print(table) |
| 277 | |
| 278 | |
| 279 | # --------------------------------------------------------------------------- |
| 280 | # Backup commands |
| 281 | # --------------------------------------------------------------------------- |
| 282 |