holocron-sync 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- holocron_sync-0.1.0/.dockerignore +7 -0
- holocron_sync-0.1.0/.github/workflows/ci.yml +31 -0
- holocron_sync-0.1.0/.github/workflows/publish.yaml +27 -0
- holocron_sync-0.1.0/.gitignore +61 -0
- holocron_sync-0.1.0/Dockerfile +22 -0
- holocron_sync-0.1.0/LICENSE +21 -0
- holocron_sync-0.1.0/PKG-INFO +84 -0
- holocron_sync-0.1.0/README.md +74 -0
- holocron_sync-0.1.0/agents.MD +178 -0
- holocron_sync-0.1.0/docker-compose.yml +11 -0
- holocron_sync-0.1.0/pyproject.toml +32 -0
- holocron_sync-0.1.0/requirements.txt +2 -0
- holocron_sync-0.1.0/src/holocron/__init__.py +3 -0
- holocron_sync-0.1.0/src/holocron/__main__.py +85 -0
- holocron_sync-0.1.0/src/holocron/config.py +36 -0
- holocron_sync-0.1.0/src/holocron/github_provider.py +99 -0
- holocron_sync-0.1.0/src/holocron/logger.py +8 -0
- holocron_sync-0.1.0/src/holocron/mirror.py +111 -0
- holocron_sync-0.1.0/tests/test_config.py +30 -0
- holocron_sync-0.1.0/tests/test_github_provider.py +72 -0
- holocron_sync-0.1.0/tests/test_holocron.py +173 -0
- holocron_sync-0.1.0/tests/test_logger.py +26 -0
- holocron_sync-0.1.0/tests/test_mirror.py +266 -0
- holocron_sync-0.1.0/uv.lock +620 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v3
|
|
21
|
+
with:
|
|
22
|
+
version: "0.4.15"
|
|
23
|
+
|
|
24
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
25
|
+
run: uv python install ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync --all-extras --dev
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: uv run pytest
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi-publish:
|
|
9
|
+
name: Upload to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment:
|
|
12
|
+
name: pypi
|
|
13
|
+
url: https://pypi.org/p/holocron-sync
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write # IMPORTANT: this permission is mandatory for Trusted Publishing
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v5
|
|
22
|
+
|
|
23
|
+
- name: Build package
|
|
24
|
+
run: uv build
|
|
25
|
+
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / Packaging
|
|
7
|
+
.Python
|
|
8
|
+
build/
|
|
9
|
+
develop-eggs/
|
|
10
|
+
dist/
|
|
11
|
+
downloads/
|
|
12
|
+
eggs/
|
|
13
|
+
.eggs/
|
|
14
|
+
lib/
|
|
15
|
+
lib64/
|
|
16
|
+
parts/
|
|
17
|
+
sdist/
|
|
18
|
+
var/
|
|
19
|
+
wheels/
|
|
20
|
+
share/python-wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
MANIFEST
|
|
25
|
+
|
|
26
|
+
# Virtual Environments
|
|
27
|
+
.env
|
|
28
|
+
.venv
|
|
29
|
+
env/
|
|
30
|
+
venv/
|
|
31
|
+
ENV/
|
|
32
|
+
env.bak/
|
|
33
|
+
venv.bak/
|
|
34
|
+
|
|
35
|
+
# Testing and Coverage
|
|
36
|
+
.coverage
|
|
37
|
+
.coverage.*
|
|
38
|
+
.cache
|
|
39
|
+
nosetests.xml
|
|
40
|
+
coverage.xml
|
|
41
|
+
*.cover
|
|
42
|
+
*.py,cover
|
|
43
|
+
.hypothesis/
|
|
44
|
+
.pytest_cache/
|
|
45
|
+
cover/
|
|
46
|
+
|
|
47
|
+
# G2G-Sync / Holocron specific
|
|
48
|
+
mirror-data/
|
|
49
|
+
*.git
|
|
50
|
+
|
|
51
|
+
# Environment Variables
|
|
52
|
+
.env
|
|
53
|
+
|
|
54
|
+
# MacOS
|
|
55
|
+
.DS_Store
|
|
56
|
+
|
|
57
|
+
# IDEs
|
|
58
|
+
.idea/
|
|
59
|
+
.vscode/
|
|
60
|
+
*.swp
|
|
61
|
+
*.swo
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
FROM python:3.12-alpine
|
|
2
|
+
|
|
3
|
+
# Install git (required for mirroring)
|
|
4
|
+
RUN apk add --no-cache git
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# Copy requirements first to leverage cache
|
|
9
|
+
COPY requirements.txt .
|
|
10
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
11
|
+
|
|
12
|
+
# Copy source code
|
|
13
|
+
COPY src/ src/
|
|
14
|
+
|
|
15
|
+
# Create directory for mirror data
|
|
16
|
+
RUN mkdir -p mirror-data
|
|
17
|
+
|
|
18
|
+
# Default entrypoint
|
|
19
|
+
ENTRYPOINT ["python", "-m", "holocron"]
|
|
20
|
+
|
|
21
|
+
# Default command (can be overridden)
|
|
22
|
+
CMD ["--watch", "--interval", "60"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Wouter Bloeyaert
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: holocron-sync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Holocron: The Ultimate Git Mirroring Tool
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: python-dotenv
|
|
8
|
+
Requires-Dist: requests>=2.28.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Holocron
|
|
12
|
+
> **The "Ultimate" Git Mirroring Tool**
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
/\
|
|
16
|
+
/ \
|
|
17
|
+
/ /\ \
|
|
18
|
+
/ / \ \
|
|
19
|
+
/ / \ \
|
|
20
|
+
/_/______\_\
|
|
21
|
+
\ \ / /
|
|
22
|
+
\ \ / /
|
|
23
|
+
\ \ / /
|
|
24
|
+
\ \/ /
|
|
25
|
+
\ /
|
|
26
|
+
\/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Holocron** is a powerful Python application designed to mirror your GitHub repositories to a local directory or a self-hosted GitLab instance. It supports parallel syncing, continuous watch mode, and local-only backups (no GitLab required).
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
- **Parallel Syncing**: Sync multiple repositories concurrently for maximum speed.
|
|
33
|
+
- **Continuous Watch Mode**: Polls for changes and syncs only when necessary (smart redundancy checks).
|
|
34
|
+
- **Two-Way Mirroring**: Creates a bare mirror (`.git` folder) for safety AND an optional checkout for visibility.
|
|
35
|
+
- **Dockerized**: Runs as a lightweight container.
|
|
36
|
+
- **Backup Level**:
|
|
37
|
+
- Full: GitHub -> Local -> GitLab
|
|
38
|
+
- Backup-Only: GitHub -> Local (No GitLab token needed)
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Docker (Recommended)
|
|
43
|
+
```bash
|
|
44
|
+
docker-compose up -d --build
|
|
45
|
+
```
|
|
46
|
+
This starts Holocron in watch mode.
|
|
47
|
+
|
|
48
|
+
### 2. Manual Run
|
|
49
|
+
```bash
|
|
50
|
+
# Install dependencies
|
|
51
|
+
uv sync
|
|
52
|
+
|
|
53
|
+
# Run a one-time backup of all your repos locally (visible files)
|
|
54
|
+
export GITHUB_TOKEN=your_token
|
|
55
|
+
uv run python src/holocron.py --backup-only --checkout --concurrency 10
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
Holocron uses environment variables for secrets:
|
|
60
|
+
|
|
61
|
+
| Variable | Description | Required |
|
|
62
|
+
| :--- | :--- | :--- |
|
|
63
|
+
| `GITHUB_TOKEN` | Your GitHub Personal Access Token (repo scope) | **Yes** |
|
|
64
|
+
| `GITLAB_TOKEN` | Your GitLab Personal Access Token (api scope) | No (if `--backup-only`) |
|
|
65
|
+
| `GITLAB_API_URL` | URL to your GitLab API (default: `http://gitlab.local/api/v4`) | No |
|
|
66
|
+
|
|
67
|
+
### Command Line Arguments
|
|
68
|
+
| Flag | Default | Description |
|
|
69
|
+
| :--- | :--- | :--- |
|
|
70
|
+
| `--watch` | False | Run continuously in a loop |
|
|
71
|
+
| `--interval` | 60 | Seconds to sleep between checks in watch mode |
|
|
72
|
+
| `--window` | 60 | Sync only repos pushed within the last N minutes |
|
|
73
|
+
| `--backup-only` | False | Mirror locally only, do not push to GitLab |
|
|
74
|
+
| `--checkout` | False | Create a visible working directory alongside the mirror |
|
|
75
|
+
| `--concurrency` | 5 | Number of parallel sync threads |
|
|
76
|
+
| `--storage` | `./mirror-data` | Directory to store repositories |
|
|
77
|
+
| `--dry-run` | False | Print what would happen without doing it |
|
|
78
|
+
| `--verbose` | False | Enable detailed debug logging |
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
Run tests with coverage:
|
|
82
|
+
```bash
|
|
83
|
+
uv run pytest --cov=src
|
|
84
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Holocron
|
|
2
|
+
> **The "Ultimate" Git Mirroring Tool**
|
|
3
|
+
|
|
4
|
+
```
|
|
5
|
+
/\
|
|
6
|
+
/ \
|
|
7
|
+
/ /\ \
|
|
8
|
+
/ / \ \
|
|
9
|
+
/ / \ \
|
|
10
|
+
/_/______\_\
|
|
11
|
+
\ \ / /
|
|
12
|
+
\ \ / /
|
|
13
|
+
\ \ / /
|
|
14
|
+
\ \/ /
|
|
15
|
+
\ /
|
|
16
|
+
\/
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Holocron** is a powerful Python application designed to mirror your GitHub repositories to a local directory or a self-hosted GitLab instance. It supports parallel syncing, continuous watch mode, and local-only backups (no GitLab required).
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
- **Parallel Syncing**: Sync multiple repositories concurrently for maximum speed.
|
|
23
|
+
- **Continuous Watch Mode**: Polls for changes and syncs only when necessary (smart redundancy checks).
|
|
24
|
+
- **Two-Way Mirroring**: Creates a bare mirror (`.git` folder) for safety AND an optional checkout for visibility.
|
|
25
|
+
- **Dockerized**: Runs as a lightweight container.
|
|
26
|
+
- **Backup Level**:
|
|
27
|
+
- Full: GitHub -> Local -> GitLab
|
|
28
|
+
- Backup-Only: GitHub -> Local (No GitLab token needed)
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Docker (Recommended)
|
|
33
|
+
```bash
|
|
34
|
+
docker-compose up -d --build
|
|
35
|
+
```
|
|
36
|
+
This starts Holocron in watch mode.
|
|
37
|
+
|
|
38
|
+
### 2. Manual Run
|
|
39
|
+
```bash
|
|
40
|
+
# Install dependencies
|
|
41
|
+
uv sync
|
|
42
|
+
|
|
43
|
+
# Run a one-time backup of all your repos locally (visible files)
|
|
44
|
+
export GITHUB_TOKEN=your_token
|
|
45
|
+
uv run python src/holocron.py --backup-only --checkout --concurrency 10
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
Holocron uses environment variables for secrets:
|
|
50
|
+
|
|
51
|
+
| Variable | Description | Required |
|
|
52
|
+
| :--- | :--- | :--- |
|
|
53
|
+
| `GITHUB_TOKEN` | Your GitHub Personal Access Token (repo scope) | **Yes** |
|
|
54
|
+
| `GITLAB_TOKEN` | Your GitLab Personal Access Token (api scope) | No (if `--backup-only`) |
|
|
55
|
+
| `GITLAB_API_URL` | URL to your GitLab API (default: `http://gitlab.local/api/v4`) | No |
|
|
56
|
+
|
|
57
|
+
### Command Line Arguments
|
|
58
|
+
| Flag | Default | Description |
|
|
59
|
+
| :--- | :--- | :--- |
|
|
60
|
+
| `--watch` | False | Run continuously in a loop |
|
|
61
|
+
| `--interval` | 60 | Seconds to sleep between checks in watch mode |
|
|
62
|
+
| `--window` | 60 | Sync only repos pushed within the last N minutes |
|
|
63
|
+
| `--backup-only` | False | Mirror locally only, do not push to GitLab |
|
|
64
|
+
| `--checkout` | False | Create a visible working directory alongside the mirror |
|
|
65
|
+
| `--concurrency` | 5 | Number of parallel sync threads |
|
|
66
|
+
| `--storage` | `./mirror-data` | Directory to store repositories |
|
|
67
|
+
| `--dry-run` | False | Print what would happen without doing it |
|
|
68
|
+
| `--verbose` | False | Enable detailed debug logging |
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
Run tests with coverage:
|
|
72
|
+
```bash
|
|
73
|
+
uv run pytest --cov=src
|
|
74
|
+
```
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## 📌 Purpose
|
|
4
|
+
|
|
5
|
+
This document defines contribution standards for both humans and AI agents working in this repo.
|
|
6
|
+
It enforces **consistency, security, and maintainability** across the codebase.
|
|
7
|
+
Think of this as our **style guide + quality contract**.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🔐 Security
|
|
12
|
+
|
|
13
|
+
* Treat all inputs as untrusted — always validate and sanitize.
|
|
14
|
+
* Avoid unsafe patterns:
|
|
15
|
+
|
|
16
|
+
* Shell exec, `eval`, unverified deserialization, raw SQL.
|
|
17
|
+
* Enforce explicit schemas (Pydantic, Django models, Marshmallow).
|
|
18
|
+
* Never expose secrets, API keys, or tokens (zero-trust assumption).
|
|
19
|
+
* Run linters, type checks, and tests before merging.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🧱 Code Design Principles
|
|
24
|
+
|
|
25
|
+
* **Clarity > Cleverness**: prioritize readability over conciseness.
|
|
26
|
+
* **Small functions**: ≤ 20 lines (target 10–15).
|
|
27
|
+
* **Single responsibility**: each function/class does one thing well.
|
|
28
|
+
* **Explicit over implicit**:
|
|
29
|
+
|
|
30
|
+
* Avoid dense one-liners, hidden side effects, and tricky language features.
|
|
31
|
+
* Prefer explicit `if` over nested ternaries.
|
|
32
|
+
* Limit call chaining to max 2 steps.
|
|
33
|
+
* **DRY but not obsessed**: reuse logic, but don’t abstract prematurely.
|
|
34
|
+
* **Consistent naming**: descriptive, not abbreviated.
|
|
35
|
+
* **Remove dead code**: no commented-out blocks or unused imports.
|
|
36
|
+
* **Documentation at point of use**: comments explain *why*, not *what*.
|
|
37
|
+
|
|
38
|
+
**Django specifics**:
|
|
39
|
+
|
|
40
|
+
* Use `bulk_create`/`bulk_update` for efficiency.
|
|
41
|
+
* Always define `UPDATE_FIELDS` constants for updates.
|
|
42
|
+
* Wrap multi-step writes in transactions.
|
|
43
|
+
* Cache repeated upserts in memory during a run.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🧰 Helpers & Utilities
|
|
48
|
+
|
|
49
|
+
* Keep helpers **local** if only used once; mark them private (`_helper`).
|
|
50
|
+
* Extract to `api/utils/<purpose>.py` if reused.
|
|
51
|
+
* Avoid generic “misc” utils files.
|
|
52
|
+
* Public helpers: type hints + docstrings; side-effect free.
|
|
53
|
+
* Private helpers: `_parse_*`, `_build_*`, `_apply_*`.
|
|
54
|
+
* Migration rule: start local, extract only when reused or file > 300 lines.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 🧩 Models
|
|
59
|
+
|
|
60
|
+
* **One file per model**: strict rule.
|
|
61
|
+
|
|
62
|
+
* Example:
|
|
63
|
+
|
|
64
|
+
* `api/models/ticker.py` → `Ticker`
|
|
65
|
+
* `api/models/exchange.py` → `Exchange`
|
|
66
|
+
* `api/models/job.py` → `Job`
|
|
67
|
+
* **Re-export** in `api/models/__init__.py`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from .ticker import Ticker
|
|
71
|
+
from .exchange import Exchange
|
|
72
|
+
from .job import Job
|
|
73
|
+
|
|
74
|
+
__all__ = ["Ticker", "Exchange", "Job"]
|
|
75
|
+
```
|
|
76
|
+
* No side effects in model files (signals, listeners go elsewhere).
|
|
77
|
+
* Admin config belongs in `admin.py`.
|
|
78
|
+
* Tests mirror model file structure.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ✅ Testing
|
|
83
|
+
|
|
84
|
+
* Every new feature requires tests. **No exceptions.**
|
|
85
|
+
* Cover: CRUD, edge cases, abuse/misuse, success paths.
|
|
86
|
+
* Tests must be deterministic and fast (<1s per unit test).
|
|
87
|
+
* Use integration tests only when needed.
|
|
88
|
+
* Test naming: `test_<functionality>_<expected_behavior>`.
|
|
89
|
+
* Test location: `/tests/` mirroring code layout.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🧩 Extensibility
|
|
94
|
+
|
|
95
|
+
* No hardcoded credentials/config.
|
|
96
|
+
* Use base classes/interfaces for provider-specific logic.
|
|
97
|
+
* Prefer loose coupling; modules should be swappable.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🗂️ File & Folder Conventions
|
|
102
|
+
|
|
103
|
+
* Each model = its own file under `src/recipes/models/`.
|
|
104
|
+
* No placeholder/empty boilerplate files.
|
|
105
|
+
* Organize by domain, not by layer (avoid `misc.py`, `common.py`).
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🧾 Documentation
|
|
110
|
+
|
|
111
|
+
* All public classes and functions need docstrings.
|
|
112
|
+
* Comments should clarify *why* decisions were made.
|
|
113
|
+
* Update Markdown/docs when behavior changes.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 💬 Commits & PRs
|
|
118
|
+
|
|
119
|
+
* **Commit title format**: `[scope] Short summary`
|
|
120
|
+
|
|
121
|
+
* Example: `[auth] Add password reset expiry`
|
|
122
|
+
* Messages must explain:
|
|
123
|
+
|
|
124
|
+
* **What** changed
|
|
125
|
+
* **Why** it’s needed
|
|
126
|
+
* **How** to test it
|
|
127
|
+
* Keep commits small and focused (atomic).
|
|
128
|
+
* No auto-generated files in commits.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🤖 Agents (AI Contribution Rules)
|
|
133
|
+
|
|
134
|
+
* Respect this document before generating code.
|
|
135
|
+
* Reuse existing patterns and naming.
|
|
136
|
+
* Never assume frameworks/libraries not already in the repo.
|
|
137
|
+
* Ensure functions are short, explicit, and modular.
|
|
138
|
+
* Include docstrings and type hints.
|
|
139
|
+
* Run security and readability checks before suggesting changes.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 🔍 Code Review Checklist
|
|
144
|
+
|
|
145
|
+
* [ ] Functions ≤ 20 lines; single responsibility
|
|
146
|
+
* [ ] No dense inline logic, no multi-statement lines
|
|
147
|
+
* [ ] Call chaining ≤ 2 steps
|
|
148
|
+
* [ ] DB ops isolated; transactions around multi-step writes
|
|
149
|
+
* [ ] Bulk ops with `UPDATE_FIELDS`
|
|
150
|
+
* [ ] No dead code; imports trimmed
|
|
151
|
+
* [ ] Comments explain *why*
|
|
152
|
+
* [ ] Tests added/updated (CRUD, edge, abuse, success paths)
|
|
153
|
+
* [ ] No secrets/unsafe code
|
|
154
|
+
* [ ] Docs updated
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 🛠️ PR Template
|
|
159
|
+
|
|
160
|
+
`.github/pull_request_template.md`:
|
|
161
|
+
|
|
162
|
+
```markdown
|
|
163
|
+
## Summary
|
|
164
|
+
- What does this change do?
|
|
165
|
+
|
|
166
|
+
## Checklist
|
|
167
|
+
- [ ] Functions ≤ 20 lines
|
|
168
|
+
- [ ] Single responsibility per function/class
|
|
169
|
+
- [ ] No chained one-liners or dense inline ternaries
|
|
170
|
+
- [ ] Intermediate variables for clarity
|
|
171
|
+
- [ ] DB I/O isolated; transactions for multi-step writes
|
|
172
|
+
- [ ] Narrow exceptions (no bare `except`)
|
|
173
|
+
- [ ] Docstrings + type hints present
|
|
174
|
+
- [ ] `UPDATE_FIELDS` constant used for bulk updates
|
|
175
|
+
- [ ] Tests mirror module layout and cover edge cases
|
|
176
|
+
|
|
177
|
+
## 🧪 Test & Quality Tools (Backend)
|
|
178
|
+
All backend test and quality tooling is executed from the project root and targets the backend codebase in `src`. Configuration files for each tool live in the backend code root (`src`).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "holocron-sync"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Holocron: The Ultimate Git Mirroring Tool"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"requests>=2.28.0",
|
|
9
|
+
"python-dotenv",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
holocron = "holocron.__main__:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/holocron"]
|
|
21
|
+
|
|
22
|
+
[tool.uv]
|
|
23
|
+
dev-dependencies = [
|
|
24
|
+
"pytest>=8.0.0",
|
|
25
|
+
"pytest-mock>=3.12.0",
|
|
26
|
+
"pytest-cov>=4.1.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
pythonpath = "src"
|
|
31
|
+
norecursedirs = ["mirror-data", ".venv", ".git"]
|
|
32
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
|
|
7
|
+
# Import from local modules
|
|
8
|
+
from .config import parse_args
|
|
9
|
+
from .logger import log
|
|
10
|
+
from .github_provider import get_github_repos
|
|
11
|
+
from .mirror import needs_sync, sync_one_repo
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
args = parse_args()
|
|
15
|
+
|
|
16
|
+
# Load secrets
|
|
17
|
+
gh_token = os.environ.get("GITHUB_TOKEN")
|
|
18
|
+
gl_token = os.environ.get("GITLAB_TOKEN")
|
|
19
|
+
|
|
20
|
+
# Validation logic
|
|
21
|
+
if not gh_token:
|
|
22
|
+
print("CRITICAL: Missing GITHUB_TOKEN.")
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
if not args.backup_only and not gl_token:
|
|
26
|
+
print("CRITICAL: Missing GITLAB_TOKEN.")
|
|
27
|
+
print("Please set GITLAB_TOKEN or use --backup-only.")
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
log("Initializing Holocron...")
|
|
31
|
+
if args.dry_run:
|
|
32
|
+
log("!!! DRY RUN MODE ACTIVE !!!")
|
|
33
|
+
|
|
34
|
+
# Track the last synced push timestamp for each repo
|
|
35
|
+
synced_pushes = {}
|
|
36
|
+
|
|
37
|
+
while True:
|
|
38
|
+
repos = get_github_repos(gh_token, args.verbose)
|
|
39
|
+
log(f"Found {len(repos)} repositories on GitHub.", is_verbose_mode=args.verbose)
|
|
40
|
+
|
|
41
|
+
sync_count = 0
|
|
42
|
+
with ThreadPoolExecutor(max_workers=args.concurrency) as executor:
|
|
43
|
+
future_to_repo = {}
|
|
44
|
+
for repo in repos:
|
|
45
|
+
repo_name = repo['name']
|
|
46
|
+
pushed_at = repo.get('pushed_at')
|
|
47
|
+
repo_dir = os.path.join(args.storage, f"{repo_name}.git")
|
|
48
|
+
|
|
49
|
+
# If watching, use the smart filter. If running once, sync all.
|
|
50
|
+
if args.watch:
|
|
51
|
+
# 1. Skip if we already synced this exact push
|
|
52
|
+
if repo_name in synced_pushes and synced_pushes[repo_name] == pushed_at:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# 2. Check time window (SKIP if old AND local repo exists)
|
|
56
|
+
# If local repo is missing, we MUST sync (bootstrap), regardless of age.
|
|
57
|
+
if os.path.exists(repo_dir) and not needs_sync(repo, args.window):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
future = executor.submit(sync_one_repo, repo, args, gh_token, gl_token)
|
|
61
|
+
future_to_repo[future] = repo
|
|
62
|
+
|
|
63
|
+
for future in as_completed(future_to_repo):
|
|
64
|
+
repo = future_to_repo[future]
|
|
65
|
+
try:
|
|
66
|
+
future.result()
|
|
67
|
+
sync_count += 1
|
|
68
|
+
# Update tracking on success
|
|
69
|
+
if repo.get('pushed_at'):
|
|
70
|
+
synced_pushes[repo['name']] = repo['pushed_at']
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
log(f"[{repo['name']}] generated an exception: {exc}")
|
|
73
|
+
|
|
74
|
+
if sync_count > 0:
|
|
75
|
+
log(f"Sync cycle complete. Updated {sync_count} repositories.")
|
|
76
|
+
elif args.verbose:
|
|
77
|
+
log("No changes detected in this cycle.")
|
|
78
|
+
|
|
79
|
+
if not args.watch:
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
time.sleep(args.interval)
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
# Load env vars from .env file
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
# --- CONFIGURATION DEFAULTS ---
|
|
9
|
+
# We use Environment Variables for security.
|
|
10
|
+
# Never hardcode passwords in open source code!
|
|
11
|
+
GITHUB_API_URL = "https://api.github.com"
|
|
12
|
+
GITLAB_API_URL = os.environ.get("GITLAB_API_URL", "http://gitlab.local/api/v4")
|
|
13
|
+
|
|
14
|
+
def parse_args():
|
|
15
|
+
"""
|
|
16
|
+
Sets up the command line arguments.
|
|
17
|
+
This allows the user to run: 'python g2g.py --dry-run'
|
|
18
|
+
"""
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description="Holocron: GitHub to GitLab/Local Mirroring Tool"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Flags (True/False options)
|
|
24
|
+
parser.add_argument("--dry-run", action="store_true", help="Simulate execution without making changes")
|
|
25
|
+
parser.add_argument("--watch", action="store_true", help="Run continuously in a loop (Daemon mode)")
|
|
26
|
+
parser.add_argument("--verbose", action="store_true", help="Print detailed logs")
|
|
27
|
+
|
|
28
|
+
# value options
|
|
29
|
+
parser.add_argument("--interval", type=int, default=60, help="Seconds to wait between checks (default: 60)")
|
|
30
|
+
parser.add_argument("--window", type=int, default=10, help="Only sync repos updated in the last X minutes")
|
|
31
|
+
parser.add_argument("--storage", type=str, default="./mirror-data", help="Local path to store git repositories")
|
|
32
|
+
parser.add_argument("--concurrency", type=int, default=5, help="Number of concurrent sync threads (default: 5)")
|
|
33
|
+
parser.add_argument("--backup-only", action="store_true", help="Mirror locally only, skip pushing to GitLab")
|
|
34
|
+
parser.add_argument("--checkout", action="store_true", help="Create a checkout of the repository alongside the mirror")
|
|
35
|
+
|
|
36
|
+
return parser.parse_args()
|