gitacross 1.0.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.
- gitacross-1.0.0/LICENSE +21 -0
- gitacross-1.0.0/PKG-INFO +318 -0
- gitacross-1.0.0/README.md +290 -0
- gitacross-1.0.0/pyproject.toml +48 -0
- gitacross-1.0.0/setup.cfg +4 -0
- gitacross-1.0.0/src/gitacross/__init__.py +34 -0
- gitacross-1.0.0/src/gitacross/__main__.py +6 -0
- gitacross-1.0.0/src/gitacross/config.py +218 -0
- gitacross-1.0.0/src/gitacross/git.py +342 -0
- gitacross-1.0.0/src/gitacross/gitea.py +9 -0
- gitacross-1.0.0/src/gitacross/github.py +9 -0
- gitacross-1.0.0/src/gitacross/linter.py +1105 -0
- gitacross-1.0.0/src/gitacross/main.py +360 -0
- gitacross-1.0.0/src/gitacross/providers/__init__.py +44 -0
- gitacross-1.0.0/src/gitacross/providers/base.py +230 -0
- gitacross-1.0.0/src/gitacross/providers/gitea.py +170 -0
- gitacross-1.0.0/src/gitacross/providers/github.py +120 -0
- gitacross-1.0.0/src/gitacross/renderer.py +195 -0
- gitacross-1.0.0/src/gitacross/retry.py +23 -0
- gitacross-1.0.0/src/gitacross/source.py +297 -0
- gitacross-1.0.0/src/gitacross/state.py +67 -0
- gitacross-1.0.0/src/gitacross/target.py +159 -0
- gitacross-1.0.0/src/gitacross.egg-info/PKG-INFO +318 -0
- gitacross-1.0.0/src/gitacross.egg-info/SOURCES.txt +27 -0
- gitacross-1.0.0/src/gitacross.egg-info/dependency_links.txt +1 -0
- gitacross-1.0.0/src/gitacross.egg-info/entry_points.txt +2 -0
- gitacross-1.0.0/src/gitacross.egg-info/requires.txt +6 -0
- gitacross-1.0.0/src/gitacross.egg-info/top_level.txt +1 -0
- gitacross-1.0.0/tests/test_sync.py +2775 -0
gitacross-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Deik
|
|
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.
|
gitacross-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitacross
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Mirror releases and git commits across platforms (Gitea, GitHub, local) with transform pipelines.
|
|
5
|
+
Author: GitAcross Authors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: git,gitea,github,mirror,sync,release
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: PyYAML<7.0,>=6.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: twine; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# GitAcross
|
|
30
|
+
|
|
31
|
+
[](https://git.nodebay.top/uqkami/GitAcross/actions)
|
|
32
|
+
|
|
33
|
+
Mirror releases between git hosts (Gitea, GitHub, local). One commit per release, linear history, with file transforms.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
local repo ──→ GitHub (publish local tags as releases)
|
|
37
|
+
Gitea ──→ GitHub (mirror dev to public)
|
|
38
|
+
GitHub ──→ local repo (backup)
|
|
39
|
+
...any combo (gitea, github, local)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
### Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Install locally in editable mode (or via pip install gitacross)
|
|
48
|
+
pip install -e .
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Run
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Lint configuration for errors, invalid keys, and redundant defaults
|
|
55
|
+
gitacross --config config.yml --lint
|
|
56
|
+
|
|
57
|
+
# Automatically fix misplaced keys and remove redundant default options
|
|
58
|
+
gitacross --config config.yml --fix
|
|
59
|
+
|
|
60
|
+
# Preview changes without modifying targets
|
|
61
|
+
gitacross --config config.yml --dry-run
|
|
62
|
+
|
|
63
|
+
# Run full sync
|
|
64
|
+
gitacross --config config.yml
|
|
65
|
+
|
|
66
|
+
# Or run directly from repository root
|
|
67
|
+
python main.py --config config.yml --lint
|
|
68
|
+
python main.py --config config.yml --fix
|
|
69
|
+
python main.py --config config.yml
|
|
70
|
+
|
|
71
|
+
# Or run as a Python module
|
|
72
|
+
python -m gitacross --config config.yml --lint
|
|
73
|
+
python -m gitacross --config config.yml --fix
|
|
74
|
+
python -m gitacross --config config.yml
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Reference
|
|
78
|
+
|
|
79
|
+
### Endpoints
|
|
80
|
+
|
|
81
|
+
| Type | `source` fields | `target` fields |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| **gitea** / **github** | `repo`, `api`, `token`<br>`mode` (default `release`, or `tag`, or `commit`)<br>`include_prereleases` (default false)<br>`include_drafts` (default false) | `repo`, `api`, `token`<br>`branch` (default main) |
|
|
84
|
+
| **local** | `path`, `tag_pattern` (default `*`) | `path`, `branch` (default main) |
|
|
85
|
+
|
|
86
|
+
Tokens use `${VAR}` syntax — resolved from environment.
|
|
87
|
+
|
|
88
|
+
`enabled` (default `true`) can be set to `false` on any project to temporarily disable or skip it without removing it from your configuration file.
|
|
89
|
+
|
|
90
|
+
`preserve_description` (default `true`, alias `preserve_release_description`) can be set at the project or endpoint level to preserve the source release notes/body on the target release, or set to `false` to leave the target release description empty.
|
|
91
|
+
|
|
92
|
+
`sync_assets` (alias `preserve_assets`, `include_assets`) is a **project-level** field that mirrors prebuilt release packages from the source to the target release — so you only need CI on the source platform:
|
|
93
|
+
|
|
94
|
+
| Value | Behaviour |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `false` (default) | No assets synced |
|
|
97
|
+
| `true` | All assets synced |
|
|
98
|
+
| `"*.tar.gz"` | Only assets matching the glob |
|
|
99
|
+
| `["*.tar.gz", "*.zip"]` | Only assets matching any listed glob |
|
|
100
|
+
|
|
101
|
+
`stream_assets: true` pairs with `sync_assets` to stream each asset upload from disk rather than buffering the full file in RAM. Default is `false`. Set to `true` when syncing large prebuilt binaries (hundreds of MB) to avoid out-of-memory errors.
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
- name: my-project
|
|
105
|
+
sync_assets: # build on Gitea, upload prebuilts to GitHub
|
|
106
|
+
- "*.tar.gz"
|
|
107
|
+
- "*.zip"
|
|
108
|
+
- "*.deb"
|
|
109
|
+
- "*-checksums.txt"
|
|
110
|
+
stream_assets: true # stream uploads from disk — avoids buffering in RAM
|
|
111
|
+
source:
|
|
112
|
+
type: gitea
|
|
113
|
+
...
|
|
114
|
+
target:
|
|
115
|
+
type: github
|
|
116
|
+
...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
### `source.mode` — release vs tag vs commit
|
|
121
|
+
|
|
122
|
+
Remote sources sync from the host's **API releases** by default (`mode: release`): prerelease/draft filtering applies, and `sync_from` must be an API release. Set `mode: tag` to treat **git tags** as releases instead — useful when tags were pushed without creating release objects:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
source:
|
|
126
|
+
type: gitea
|
|
127
|
+
repo: owner/repo
|
|
128
|
+
api: https://gitea.example.com/api/v1
|
|
129
|
+
token: ${GITEA_TOKEN}
|
|
130
|
+
mode: tag
|
|
131
|
+
sync_from: v2.0.0
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
In `release` mode, a `sync_from` tag that exists only in git (no release object) or is filtered out as prerelease/draft produces a warning and syncs nothing, pointing you at the right option — `mode: tag` or `include_prereleases`/`include_drafts` — instead of silently treating tags as releases.
|
|
135
|
+
|
|
136
|
+
Synced state is keyed by **tag name**, so switching a repo between `release` and `tag` modes is safe: already-synced tags are skipped regardless of the current mode (older state files keyed by API release id are migrated automatically).
|
|
137
|
+
|
|
138
|
+
#### `mode: commit` — sync latest HEAD instead of releases
|
|
139
|
+
|
|
140
|
+
Set `mode: commit` to sync the **current HEAD of the source branch** each time the script runs, rather than iterating over releases or tags. No tag or release is created on the target — only a plain commit is pushed.
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
source:
|
|
144
|
+
type: gitea
|
|
145
|
+
repo: owner/repo
|
|
146
|
+
api: https://gitea.example.com/api/v1
|
|
147
|
+
token: ${GITEA_TOKEN}
|
|
148
|
+
mode: commit
|
|
149
|
+
branch: main # optional — which branch to read HEAD from (auto-detected if omitted)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| Behaviour | Detail |
|
|
153
|
+
|---|---|
|
|
154
|
+
| **What gets synced** | Single snapshot of the current branch HEAD |
|
|
155
|
+
| **No tag or release** | Only a plain commit is pushed to the target branch |
|
|
156
|
+
| **`branch`** | Which source branch to read HEAD from. Auto-detects `origin/HEAD`, then tries `main`/`master`/`trunk` |
|
|
157
|
+
| **State key** | Commit SHA (not tag name). Already-synced SHAs are skipped |
|
|
158
|
+
| **Idempotent** | Re-running with same HEAD is a no-op (same SHA already in state) |
|
|
159
|
+
| **State purged** | Re-commits current HEAD snapshot; git sees no diff if nothing changed → no-op commit |
|
|
160
|
+
|
|
161
|
+
### `renderer.ignore`
|
|
162
|
+
|
|
163
|
+
A list of glob patterns. Matched paths are removed before any operations run.
|
|
164
|
+
|
|
165
|
+
| Wildcard | Meaning |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `*` | Matches within a single path segment (does **not** cross `/`) |
|
|
168
|
+
| `**` | Matches across any number of directory levels (recursive) |
|
|
169
|
+
|
|
170
|
+
So `.git/*` would only match direct children like `.git/config`, but miss `.git/refs/heads/main`. Use `.git/**` to delete everything inside. Also, matching the directory name directly (`node_modules`) will remove the whole tree in one shot via `shutil.rmtree`, which is slightly faster than matching each file individually with `node_modules/**`.
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
ignore:
|
|
174
|
+
- node_modules # any node_modules/ dir, at any depth
|
|
175
|
+
- "*.secret" # only in root (single *, no /)
|
|
176
|
+
- "build/**/*.o" # any .o file under any build/ dir
|
|
177
|
+
- ToDo.md # any file named ToDo.md, at any depth
|
|
178
|
+
- some_folder/node_modules # node_modules only when inside some_folder/
|
|
179
|
+
- "./some_folder/node_modules" # root-only variant (anchored to ./)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `renderer.operations`
|
|
183
|
+
|
|
184
|
+
All operations run top-to-bottom in the order they're listed. This applies at every level:
|
|
185
|
+
|
|
186
|
+
- **Operation blocks** run in order (e.g. `remove` before `replace` before `add`)
|
|
187
|
+
- **Items inside each block** also run in order (e.g. second `replace` item runs after the first)
|
|
188
|
+
|
|
189
|
+
This matters when later steps depend on earlier ones — for example, a `rename` moving a file, then a `replace` modifying the renamed target.
|
|
190
|
+
|
|
191
|
+
#### `remove`
|
|
192
|
+
|
|
193
|
+
| Field | Required | Default | Description |
|
|
194
|
+
|---|---|---|---|
|
|
195
|
+
| `path` | yes | — | Path or pattern to remove |
|
|
196
|
+
| `pattern` | no | `literal` | `literal`, `glob`, or `regex` |
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
- remove:
|
|
200
|
+
- path: .gitea # literal path
|
|
201
|
+
- path: "*.secret"
|
|
202
|
+
pattern: glob # glob pattern
|
|
203
|
+
- path: "build\\d+" # regex matches path
|
|
204
|
+
pattern: regex
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### `rename`
|
|
208
|
+
|
|
209
|
+
| Field | Required | Default | Description |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| `from` | yes | — | Source path |
|
|
212
|
+
| `to` | yes | — | Destination path |
|
|
213
|
+
| `pattern` | no | `literal` | Only `literal` is implemented |
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
- rename:
|
|
217
|
+
- from: .gitea
|
|
218
|
+
to: .github
|
|
219
|
+
pattern: literal
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### `replace`
|
|
223
|
+
|
|
224
|
+
| Field | Required | Default | Description |
|
|
225
|
+
|---|---|---|---|
|
|
226
|
+
| `search` | yes | — | String (literal) or pattern (regex) to find |
|
|
227
|
+
| `replace` | yes | — | Replacement text |
|
|
228
|
+
| `pattern` | no | `literal` | `literal` or `regex` |
|
|
229
|
+
| `glob` | no | all files | Only modify files matching this glob |
|
|
230
|
+
| `path` | no | — | Only modify this exact relative file path (takes precedence over `glob`) |
|
|
231
|
+
|
|
232
|
+
Only UTF-8 text files are scanned. Binary files are skipped.
|
|
233
|
+
|
|
234
|
+
```yaml
|
|
235
|
+
- replace:
|
|
236
|
+
- search: https://gitea\.example\.com
|
|
237
|
+
replace: https://github.com
|
|
238
|
+
pattern: regex
|
|
239
|
+
glob: "*.md"
|
|
240
|
+
- search: http://old-url.com
|
|
241
|
+
replace: https://new-url.com
|
|
242
|
+
pattern: literal
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### `add`
|
|
246
|
+
|
|
247
|
+
| Field | Required | Description |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| `path` | yes | File path to create (parent dirs auto-created) |
|
|
250
|
+
| `content` | yes | File contents |
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
- add:
|
|
254
|
+
- path: .github/FUNDING.yml
|
|
255
|
+
content: |
|
|
256
|
+
github: myuser
|
|
257
|
+
- path: RELEASE_NOTES.md
|
|
258
|
+
content: |
|
|
259
|
+
# Release Notes
|
|
260
|
+
...
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### `validate`
|
|
264
|
+
|
|
265
|
+
| Field | Required | Description |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| `assert` | yes | `file_exists`, `file_absent`, `string_exists`, `string_absent` |
|
|
268
|
+
| `path` | yes | File path to check |
|
|
269
|
+
| `pattern` | for string checks | Text to search for |
|
|
270
|
+
|
|
271
|
+
Aborts the entire release if any assertion fails.
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
- validate:
|
|
275
|
+
- assert: file_exists
|
|
276
|
+
path: README.md
|
|
277
|
+
- assert: string_absent
|
|
278
|
+
path: LICENSE
|
|
279
|
+
pattern: "Gitea"
|
|
280
|
+
- assert: string_exists
|
|
281
|
+
path: README.md
|
|
282
|
+
pattern: "MIT"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### `retry`
|
|
286
|
+
|
|
287
|
+
| Field | Default | Description |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `max_attempts` | 3 | Number of retries before giving up |
|
|
290
|
+
| `backoff_seconds` | 2 | Base delay (doubles each attempt) |
|
|
291
|
+
|
|
292
|
+
### CLI
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
python -m sync.main --config PATH [--project NAME] [--dry-run] [-v]
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
| Flag | Description |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `--config` | Path to config file (required) |
|
|
301
|
+
| `--project` | Sync only one project (by name) |
|
|
302
|
+
| `--dry-run` | Preview changes without committing or pushing |
|
|
303
|
+
| `-v` | Debug logging |
|
|
304
|
+
|
|
305
|
+
## How it works
|
|
306
|
+
|
|
307
|
+
1. Fetch releases from source (paginated API or local tags)
|
|
308
|
+
2. Filter out already-synced releases (tracked in `.gitsync/state.yml`)
|
|
309
|
+
3. For each new release (oldest first):
|
|
310
|
+
- Export tag's file tree via `git archive`
|
|
311
|
+
- Apply `ignore` patterns, then `operations` in order
|
|
312
|
+
- Commit on top of target branch (linear history)
|
|
313
|
+
- Create annotated tag
|
|
314
|
+
- Push (remote targets) or populate working tree (local targets)
|
|
315
|
+
- Create release via API (remote targets)
|
|
316
|
+
4. Persist state atomically
|
|
317
|
+
|
|
318
|
+
Re-running is idempotent — already-synced releases are skipped.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# GitAcross
|
|
2
|
+
|
|
3
|
+
[](https://git.nodebay.top/uqkami/GitAcross/actions)
|
|
4
|
+
|
|
5
|
+
Mirror releases between git hosts (Gitea, GitHub, local). One commit per release, linear history, with file transforms.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
local repo ──→ GitHub (publish local tags as releases)
|
|
9
|
+
Gitea ──→ GitHub (mirror dev to public)
|
|
10
|
+
GitHub ──→ local repo (backup)
|
|
11
|
+
...any combo (gitea, github, local)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
### Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install locally in editable mode (or via pip install gitacross)
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Run
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Lint configuration for errors, invalid keys, and redundant defaults
|
|
27
|
+
gitacross --config config.yml --lint
|
|
28
|
+
|
|
29
|
+
# Automatically fix misplaced keys and remove redundant default options
|
|
30
|
+
gitacross --config config.yml --fix
|
|
31
|
+
|
|
32
|
+
# Preview changes without modifying targets
|
|
33
|
+
gitacross --config config.yml --dry-run
|
|
34
|
+
|
|
35
|
+
# Run full sync
|
|
36
|
+
gitacross --config config.yml
|
|
37
|
+
|
|
38
|
+
# Or run directly from repository root
|
|
39
|
+
python main.py --config config.yml --lint
|
|
40
|
+
python main.py --config config.yml --fix
|
|
41
|
+
python main.py --config config.yml
|
|
42
|
+
|
|
43
|
+
# Or run as a Python module
|
|
44
|
+
python -m gitacross --config config.yml --lint
|
|
45
|
+
python -m gitacross --config config.yml --fix
|
|
46
|
+
python -m gitacross --config config.yml
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Reference
|
|
50
|
+
|
|
51
|
+
### Endpoints
|
|
52
|
+
|
|
53
|
+
| Type | `source` fields | `target` fields |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| **gitea** / **github** | `repo`, `api`, `token`<br>`mode` (default `release`, or `tag`, or `commit`)<br>`include_prereleases` (default false)<br>`include_drafts` (default false) | `repo`, `api`, `token`<br>`branch` (default main) |
|
|
56
|
+
| **local** | `path`, `tag_pattern` (default `*`) | `path`, `branch` (default main) |
|
|
57
|
+
|
|
58
|
+
Tokens use `${VAR}` syntax — resolved from environment.
|
|
59
|
+
|
|
60
|
+
`enabled` (default `true`) can be set to `false` on any project to temporarily disable or skip it without removing it from your configuration file.
|
|
61
|
+
|
|
62
|
+
`preserve_description` (default `true`, alias `preserve_release_description`) can be set at the project or endpoint level to preserve the source release notes/body on the target release, or set to `false` to leave the target release description empty.
|
|
63
|
+
|
|
64
|
+
`sync_assets` (alias `preserve_assets`, `include_assets`) is a **project-level** field that mirrors prebuilt release packages from the source to the target release — so you only need CI on the source platform:
|
|
65
|
+
|
|
66
|
+
| Value | Behaviour |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `false` (default) | No assets synced |
|
|
69
|
+
| `true` | All assets synced |
|
|
70
|
+
| `"*.tar.gz"` | Only assets matching the glob |
|
|
71
|
+
| `["*.tar.gz", "*.zip"]` | Only assets matching any listed glob |
|
|
72
|
+
|
|
73
|
+
`stream_assets: true` pairs with `sync_assets` to stream each asset upload from disk rather than buffering the full file in RAM. Default is `false`. Set to `true` when syncing large prebuilt binaries (hundreds of MB) to avoid out-of-memory errors.
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
- name: my-project
|
|
77
|
+
sync_assets: # build on Gitea, upload prebuilts to GitHub
|
|
78
|
+
- "*.tar.gz"
|
|
79
|
+
- "*.zip"
|
|
80
|
+
- "*.deb"
|
|
81
|
+
- "*-checksums.txt"
|
|
82
|
+
stream_assets: true # stream uploads from disk — avoids buffering in RAM
|
|
83
|
+
source:
|
|
84
|
+
type: gitea
|
|
85
|
+
...
|
|
86
|
+
target:
|
|
87
|
+
type: github
|
|
88
|
+
...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
### `source.mode` — release vs tag vs commit
|
|
93
|
+
|
|
94
|
+
Remote sources sync from the host's **API releases** by default (`mode: release`): prerelease/draft filtering applies, and `sync_from` must be an API release. Set `mode: tag` to treat **git tags** as releases instead — useful when tags were pushed without creating release objects:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
source:
|
|
98
|
+
type: gitea
|
|
99
|
+
repo: owner/repo
|
|
100
|
+
api: https://gitea.example.com/api/v1
|
|
101
|
+
token: ${GITEA_TOKEN}
|
|
102
|
+
mode: tag
|
|
103
|
+
sync_from: v2.0.0
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
In `release` mode, a `sync_from` tag that exists only in git (no release object) or is filtered out as prerelease/draft produces a warning and syncs nothing, pointing you at the right option — `mode: tag` or `include_prereleases`/`include_drafts` — instead of silently treating tags as releases.
|
|
107
|
+
|
|
108
|
+
Synced state is keyed by **tag name**, so switching a repo between `release` and `tag` modes is safe: already-synced tags are skipped regardless of the current mode (older state files keyed by API release id are migrated automatically).
|
|
109
|
+
|
|
110
|
+
#### `mode: commit` — sync latest HEAD instead of releases
|
|
111
|
+
|
|
112
|
+
Set `mode: commit` to sync the **current HEAD of the source branch** each time the script runs, rather than iterating over releases or tags. No tag or release is created on the target — only a plain commit is pushed.
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
source:
|
|
116
|
+
type: gitea
|
|
117
|
+
repo: owner/repo
|
|
118
|
+
api: https://gitea.example.com/api/v1
|
|
119
|
+
token: ${GITEA_TOKEN}
|
|
120
|
+
mode: commit
|
|
121
|
+
branch: main # optional — which branch to read HEAD from (auto-detected if omitted)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Behaviour | Detail |
|
|
125
|
+
|---|---|
|
|
126
|
+
| **What gets synced** | Single snapshot of the current branch HEAD |
|
|
127
|
+
| **No tag or release** | Only a plain commit is pushed to the target branch |
|
|
128
|
+
| **`branch`** | Which source branch to read HEAD from. Auto-detects `origin/HEAD`, then tries `main`/`master`/`trunk` |
|
|
129
|
+
| **State key** | Commit SHA (not tag name). Already-synced SHAs are skipped |
|
|
130
|
+
| **Idempotent** | Re-running with same HEAD is a no-op (same SHA already in state) |
|
|
131
|
+
| **State purged** | Re-commits current HEAD snapshot; git sees no diff if nothing changed → no-op commit |
|
|
132
|
+
|
|
133
|
+
### `renderer.ignore`
|
|
134
|
+
|
|
135
|
+
A list of glob patterns. Matched paths are removed before any operations run.
|
|
136
|
+
|
|
137
|
+
| Wildcard | Meaning |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `*` | Matches within a single path segment (does **not** cross `/`) |
|
|
140
|
+
| `**` | Matches across any number of directory levels (recursive) |
|
|
141
|
+
|
|
142
|
+
So `.git/*` would only match direct children like `.git/config`, but miss `.git/refs/heads/main`. Use `.git/**` to delete everything inside. Also, matching the directory name directly (`node_modules`) will remove the whole tree in one shot via `shutil.rmtree`, which is slightly faster than matching each file individually with `node_modules/**`.
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
ignore:
|
|
146
|
+
- node_modules # any node_modules/ dir, at any depth
|
|
147
|
+
- "*.secret" # only in root (single *, no /)
|
|
148
|
+
- "build/**/*.o" # any .o file under any build/ dir
|
|
149
|
+
- ToDo.md # any file named ToDo.md, at any depth
|
|
150
|
+
- some_folder/node_modules # node_modules only when inside some_folder/
|
|
151
|
+
- "./some_folder/node_modules" # root-only variant (anchored to ./)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `renderer.operations`
|
|
155
|
+
|
|
156
|
+
All operations run top-to-bottom in the order they're listed. This applies at every level:
|
|
157
|
+
|
|
158
|
+
- **Operation blocks** run in order (e.g. `remove` before `replace` before `add`)
|
|
159
|
+
- **Items inside each block** also run in order (e.g. second `replace` item runs after the first)
|
|
160
|
+
|
|
161
|
+
This matters when later steps depend on earlier ones — for example, a `rename` moving a file, then a `replace` modifying the renamed target.
|
|
162
|
+
|
|
163
|
+
#### `remove`
|
|
164
|
+
|
|
165
|
+
| Field | Required | Default | Description |
|
|
166
|
+
|---|---|---|---|
|
|
167
|
+
| `path` | yes | — | Path or pattern to remove |
|
|
168
|
+
| `pattern` | no | `literal` | `literal`, `glob`, or `regex` |
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
- remove:
|
|
172
|
+
- path: .gitea # literal path
|
|
173
|
+
- path: "*.secret"
|
|
174
|
+
pattern: glob # glob pattern
|
|
175
|
+
- path: "build\\d+" # regex matches path
|
|
176
|
+
pattern: regex
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### `rename`
|
|
180
|
+
|
|
181
|
+
| Field | Required | Default | Description |
|
|
182
|
+
|---|---|---|---|
|
|
183
|
+
| `from` | yes | — | Source path |
|
|
184
|
+
| `to` | yes | — | Destination path |
|
|
185
|
+
| `pattern` | no | `literal` | Only `literal` is implemented |
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
- rename:
|
|
189
|
+
- from: .gitea
|
|
190
|
+
to: .github
|
|
191
|
+
pattern: literal
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### `replace`
|
|
195
|
+
|
|
196
|
+
| Field | Required | Default | Description |
|
|
197
|
+
|---|---|---|---|
|
|
198
|
+
| `search` | yes | — | String (literal) or pattern (regex) to find |
|
|
199
|
+
| `replace` | yes | — | Replacement text |
|
|
200
|
+
| `pattern` | no | `literal` | `literal` or `regex` |
|
|
201
|
+
| `glob` | no | all files | Only modify files matching this glob |
|
|
202
|
+
| `path` | no | — | Only modify this exact relative file path (takes precedence over `glob`) |
|
|
203
|
+
|
|
204
|
+
Only UTF-8 text files are scanned. Binary files are skipped.
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
- replace:
|
|
208
|
+
- search: https://gitea\.example\.com
|
|
209
|
+
replace: https://github.com
|
|
210
|
+
pattern: regex
|
|
211
|
+
glob: "*.md"
|
|
212
|
+
- search: http://old-url.com
|
|
213
|
+
replace: https://new-url.com
|
|
214
|
+
pattern: literal
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### `add`
|
|
218
|
+
|
|
219
|
+
| Field | Required | Description |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| `path` | yes | File path to create (parent dirs auto-created) |
|
|
222
|
+
| `content` | yes | File contents |
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
- add:
|
|
226
|
+
- path: .github/FUNDING.yml
|
|
227
|
+
content: |
|
|
228
|
+
github: myuser
|
|
229
|
+
- path: RELEASE_NOTES.md
|
|
230
|
+
content: |
|
|
231
|
+
# Release Notes
|
|
232
|
+
...
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### `validate`
|
|
236
|
+
|
|
237
|
+
| Field | Required | Description |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `assert` | yes | `file_exists`, `file_absent`, `string_exists`, `string_absent` |
|
|
240
|
+
| `path` | yes | File path to check |
|
|
241
|
+
| `pattern` | for string checks | Text to search for |
|
|
242
|
+
|
|
243
|
+
Aborts the entire release if any assertion fails.
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
- validate:
|
|
247
|
+
- assert: file_exists
|
|
248
|
+
path: README.md
|
|
249
|
+
- assert: string_absent
|
|
250
|
+
path: LICENSE
|
|
251
|
+
pattern: "Gitea"
|
|
252
|
+
- assert: string_exists
|
|
253
|
+
path: README.md
|
|
254
|
+
pattern: "MIT"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `retry`
|
|
258
|
+
|
|
259
|
+
| Field | Default | Description |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `max_attempts` | 3 | Number of retries before giving up |
|
|
262
|
+
| `backoff_seconds` | 2 | Base delay (doubles each attempt) |
|
|
263
|
+
|
|
264
|
+
### CLI
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
python -m sync.main --config PATH [--project NAME] [--dry-run] [-v]
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
| Flag | Description |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `--config` | Path to config file (required) |
|
|
273
|
+
| `--project` | Sync only one project (by name) |
|
|
274
|
+
| `--dry-run` | Preview changes without committing or pushing |
|
|
275
|
+
| `-v` | Debug logging |
|
|
276
|
+
|
|
277
|
+
## How it works
|
|
278
|
+
|
|
279
|
+
1. Fetch releases from source (paginated API or local tags)
|
|
280
|
+
2. Filter out already-synced releases (tracked in `.gitsync/state.yml`)
|
|
281
|
+
3. For each new release (oldest first):
|
|
282
|
+
- Export tag's file tree via `git archive`
|
|
283
|
+
- Apply `ignore` patterns, then `operations` in order
|
|
284
|
+
- Commit on top of target branch (linear history)
|
|
285
|
+
- Create annotated tag
|
|
286
|
+
- Push (remote targets) or populate working tree (local targets)
|
|
287
|
+
- Create release via API (remote targets)
|
|
288
|
+
4. Persist state atomically
|
|
289
|
+
|
|
290
|
+
Re-running is idempotent — already-synced releases are skipped.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gitacross"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Mirror releases and git commits across platforms (Gitea, GitHub, local) with transform pipelines."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "GitAcross Authors" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["git", "gitea", "github", "mirror", "sync", "release"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"PyYAML>=6.0,<7.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"build",
|
|
37
|
+
"twine",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
gitacross = "gitacross.main:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
pythonpath = ["src"]
|
|
48
|
+
testpaths = ["tests"]
|