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.
@@ -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.
@@ -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
+ [![CI](https://git.nodebay.top/uqkami/GitAcross/actions/workflows/test.yml/badge.svg)](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
+ [![CI](https://git.nodebay.top/uqkami/GitAcross/actions/workflows/test.yml/badge.svg)](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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+