pubgate 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.
Files changed (49) hide show
  1. pubgate-0.1.0/.flake8 +2 -0
  2. pubgate-0.1.0/.gitattributes +1 -0
  3. pubgate-0.1.0/.github/dependabot.yml +6 -0
  4. pubgate-0.1.0/.github/workflows/pre-commit.yml +31 -0
  5. pubgate-0.1.0/.github/workflows/publish-to-pypi.yml +79 -0
  6. pubgate-0.1.0/.github/workflows/pytest.yml +48 -0
  7. pubgate-0.1.0/.gitignore +3 -0
  8. pubgate-0.1.0/.pre-commit-config.yaml +25 -0
  9. pubgate-0.1.0/.python-version +1 -0
  10. pubgate-0.1.0/LICENSE +21 -0
  11. pubgate-0.1.0/PKG-INFO +317 -0
  12. pubgate-0.1.0/README.md +292 -0
  13. pubgate-0.1.0/SPEC.md +158 -0
  14. pubgate-0.1.0/pubgate/__init__.py +4 -0
  15. pubgate-0.1.0/pubgate/__main__.py +97 -0
  16. pubgate-0.1.0/pubgate/_log.py +42 -0
  17. pubgate-0.1.0/pubgate/absorb.py +302 -0
  18. pubgate-0.1.0/pubgate/config.py +157 -0
  19. pubgate-0.1.0/pubgate/core.py +611 -0
  20. pubgate-0.1.0/pubgate/errors.py +10 -0
  21. pubgate-0.1.0/pubgate/filtering.py +74 -0
  22. pubgate-0.1.0/pubgate/git.py +461 -0
  23. pubgate-0.1.0/pubgate/models.py +38 -0
  24. pubgate-0.1.0/pubgate/pr.py +374 -0
  25. pubgate-0.1.0/pubgate/publish.py +68 -0
  26. pubgate-0.1.0/pubgate/stage_snapshot.py +124 -0
  27. pubgate-0.1.0/pubgate/state.py +44 -0
  28. pubgate-0.1.0/pubgate/status.py +198 -0
  29. pubgate-0.1.0/pubgate.egg-info/PKG-INFO +317 -0
  30. pubgate-0.1.0/pubgate.egg-info/SOURCES.txt +47 -0
  31. pubgate-0.1.0/pubgate.egg-info/dependency_links.txt +1 -0
  32. pubgate-0.1.0/pubgate.egg-info/entry_points.txt +2 -0
  33. pubgate-0.1.0/pubgate.egg-info/requires.txt +4 -0
  34. pubgate-0.1.0/pubgate.egg-info/top_level.txt +1 -0
  35. pubgate-0.1.0/pyproject.toml +62 -0
  36. pubgate-0.1.0/setup.cfg +4 -0
  37. pubgate-0.1.0/tests/conftest.py +332 -0
  38. pubgate-0.1.0/tests/test_absorb.py +637 -0
  39. pubgate-0.1.0/tests/test_cli.py +326 -0
  40. pubgate-0.1.0/tests/test_filtering.py +152 -0
  41. pubgate-0.1.0/tests/test_lfs.py +393 -0
  42. pubgate-0.1.0/tests/test_logging.py +74 -0
  43. pubgate-0.1.0/tests/test_pr.py +698 -0
  44. pubgate-0.1.0/tests/test_publish.py +505 -0
  45. pubgate-0.1.0/tests/test_recovery.py +73 -0
  46. pubgate-0.1.0/tests/test_stage.py +413 -0
  47. pubgate-0.1.0/tests/test_startup.py +45 -0
  48. pubgate-0.1.0/tests/test_status.py +273 -0
  49. pubgate-0.1.0/uv.lock +459 -0
pubgate-0.1.0/.flake8 ADDED
@@ -0,0 +1,2 @@
1
+ [flake8]
2
+ max-line-length = 120
@@ -0,0 +1 @@
1
+ * text=auto
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "monthly"
@@ -0,0 +1,31 @@
1
+ name: Pre-commit
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ pre-commit:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+
22
+ - uses: actions/setup-python@v6
23
+ with:
24
+ python-version: "3.12"
25
+
26
+ - uses: astral-sh/setup-uv@v7
27
+
28
+ - name: Install project (for ty import resolution)
29
+ run: uv sync
30
+
31
+ - uses: pre-commit/action@v3.0.1
@@ -0,0 +1,79 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ outputs:
12
+ is-prerelease: ${{ steps.check-tag.outputs.is-prerelease }}
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+ with:
16
+ fetch-depth: 0
17
+
18
+ - name: Check if pre-release tag
19
+ id: check-tag
20
+ run: |
21
+ TAG="${GITHUB_REF_NAME}"
22
+ if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
23
+ echo "is-prerelease=false" >> "$GITHUB_OUTPUT"
24
+ else
25
+ echo "is-prerelease=true" >> "$GITHUB_OUTPUT"
26
+ fi
27
+
28
+ - uses: astral-sh/setup-uv@v7
29
+
30
+ - name: Build package
31
+ run: uv build
32
+
33
+ - name: Upload dist artifacts
34
+ uses: actions/upload-artifact@v4
35
+ with:
36
+ name: python-package-distributions
37
+ path: dist/
38
+
39
+ publish-testpypi:
40
+ name: Publish to TestPyPI
41
+ if: needs.build.outputs.is-prerelease == 'true'
42
+ needs: build
43
+ runs-on: ubuntu-latest
44
+ environment:
45
+ name: testpypi
46
+ url: https://test.pypi.org/p/pubgate
47
+ permissions:
48
+ id-token: write
49
+ steps:
50
+ - name: Download dist artifacts
51
+ uses: actions/download-artifact@v4
52
+ with:
53
+ name: python-package-distributions
54
+ path: dist/
55
+
56
+ - name: Publish to TestPyPI
57
+ uses: pypa/gh-action-pypi-publish@release/v1
58
+ with:
59
+ repository-url: https://test.pypi.org/legacy/
60
+
61
+ publish-pypi:
62
+ name: Publish to PyPI
63
+ if: needs.build.outputs.is-prerelease == 'false'
64
+ needs: build
65
+ runs-on: ubuntu-latest
66
+ environment:
67
+ name: pypi
68
+ url: https://pypi.org/p/pubgate
69
+ permissions:
70
+ id-token: write
71
+ steps:
72
+ - name: Download dist artifacts
73
+ uses: actions/download-artifact@v4
74
+ with:
75
+ name: python-package-distributions
76
+ path: dist/
77
+
78
+ - name: Publish to PyPI
79
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,48 @@
1
+ name: Pytest
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ test:
18
+ timeout-minutes: 15
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ os: [ubuntu-latest, windows-latest, macos-latest]
23
+ python: ["3.12"]
24
+ runs-on: ${{ matrix.os }}
25
+ steps:
26
+ - uses: actions/checkout@v6
27
+ with:
28
+ fetch-depth: 0
29
+
30
+ - name: Setup Python ${{ matrix.python }}
31
+ uses: actions/setup-python@v6
32
+ with:
33
+ python-version: ${{ matrix.python }}
34
+
35
+ - uses: astral-sh/setup-uv@v7
36
+ with:
37
+ enable-cache: true
38
+
39
+ - name: Install dependencies
40
+ run: uv sync --python ${{ matrix.python }}
41
+
42
+ - name: Configure git
43
+ run: |
44
+ git config --global user.name "CI"
45
+ git config --global user.email "ci@test.local"
46
+
47
+ - name: Test with pytest
48
+ run: uv run pytest -v -n auto
@@ -0,0 +1,3 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
@@ -0,0 +1,25 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: check-yaml
6
+ - id: check-toml
7
+ - id: end-of-file-fixer
8
+ - id: trailing-whitespace
9
+ - id: check-merge-conflict
10
+
11
+ - repo: https://github.com/astral-sh/ruff-pre-commit
12
+ rev: v0.9.4
13
+ hooks:
14
+ - id: ruff
15
+ args: [--fix]
16
+ - id: ruff-format
17
+
18
+ - repo: local
19
+ hooks:
20
+ - id: ty
21
+ name: ty
22
+ entry: uvx ty check
23
+ language: system
24
+ types: [python]
25
+ pass_filenames: false
@@ -0,0 +1 @@
1
+ 3.12
pubgate-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ardi Loot
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.
pubgate-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,317 @@
1
+ Metadata-Version: 2.4
2
+ Name: pubgate
3
+ Version: 0.1.0
4
+ Summary: Sync an internal repo with a public repo through reviewed PRs
5
+ Author: Ardi Loot
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ardiloot/pubgate
8
+ Project-URL: Repository, https://github.com/ardiloot/pubgate
9
+ Project-URL: Issues, https://github.com/ardiloot/pubgate/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Version Control :: Git
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: colorlog
23
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
24
+ Dynamic: license-file
25
+
26
+ # pubgate
27
+
28
+ [![Pre-commit](https://github.com/ardiloot/pubgate/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/ardiloot/pubgate/actions/workflows/pre-commit.yml)
29
+ [![Pytest](https://github.com/ardiloot/pubgate/actions/workflows/pytest.yml/badge.svg)](https://github.com/ardiloot/pubgate/actions/workflows/pytest.yml)
30
+
31
+ Safe bidirectional sync between internal and public git repos.
32
+
33
+ ## The Problem
34
+
35
+ You have an internal repo with proprietary code and you want to open-source parts of it. This creates two ongoing problems:
36
+
37
+ 1. **Leak risk.** Internal files, code sections, and commit history must never reach the public repo. Standard git tools (fork, merge, cherry-pick) all carry or expose internal commits, and a single misstep exposes everything. `git filter-repo` can strip history once, but rewrites SHAs on every run, breaking external clones and making it unusable for continuous sync.
38
+
39
+ 2. **Silent divergence.** Once two repos exist, they drift apart. Public contributions arrive on one side, internal development continues on the other, and without a disciplined process the repos become increasingly hard to reconcile: patches stop applying, filtered content falls out of sync, and nobody notices until it's a project to fix.
40
+
41
+ ## What It Does
42
+
43
+ pubgate prepares branches for review. You create and merge PRs on your git host (GitHub, GitLab, etc.). It handles both directions:
44
+
45
+ - Stage changes behind an internal leak-review PR gate
46
+ - Push reviewed content to a PR branch on the public repo
47
+ - Absorb public contributions back into internal main with three-way merge
48
+
49
+ Filtering is mechanical: built-in ignore patterns exclude common internal/private/secret file naming conventions out of the box, `BEGIN-INTERNAL` / `END-INTERNAL` markers strip sections from individual files, and `pubgate.toml` is always excluded automatically. Custom `ignore` patterns in the config replace the defaults.
50
+
51
+ **Core principle:** the public repo is always an exact filtered copy of internal, never an independent fork. If external contributions arrive mid-cycle, `publish` bases the public PR on the last absorbed commit; git's three-way merge preserves them or surfaces conflicts. Divergence stays controlled and bounded.
52
+
53
+ The two workflows:
54
+
55
+ <table>
56
+ <tr>
57
+ <td valign="top" width="50%"><strong>Making changes public: absorb → stage → publish</strong><br><em>absorb is recommended first if the public repo has unabsorbed changes, but stage and publish proceed either way</em>
58
+
59
+ ```mermaid
60
+ %%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 25, "rankSpacing": 30, "padding": 10}}}%%
61
+ flowchart TD
62
+ A["🔒 main (internal)"]
63
+ A -->|"stage"| B["🔒 pubgate/stage (internal)"]
64
+ B -->|"PR · leak review"| C["🔒 pubgate/public-approved (internal)"]
65
+ C ==>|"publish"| D["🌐 pubgate/publish (public)"]
66
+ D -->|"PR · publish check"| E["🌐 main (public)"]
67
+
68
+ style A fill:#2d5a2d,stroke:#4a4,color:#fff
69
+ style B fill:#1a3a1a,stroke:#4a4,color:#ccc
70
+ style C fill:#2d5a2d,stroke:#4a4,color:#fff
71
+ style D fill:#1a3a5a,stroke:#48f,color:#ccc
72
+ style E fill:#2d3a5a,stroke:#48f,color:#fff
73
+ ```
74
+
75
+ </td>
76
+ <td valign="top" width="50%"><strong>Incorporating public contributions: absorb</strong><br><em>Run when the public repo has external contributions</em>
77
+
78
+ ```mermaid
79
+ %%{init: {"flowchart": {"useMaxWidth": false, "nodeSpacing": 25, "rankSpacing": 30, "padding": 10}}}%%
80
+ flowchart TD
81
+ E2["🌐 main (public)"]
82
+ E2 -->|"absorb"| F["🔒 pubgate/absorb (internal)"]
83
+ F -->|"PR · merge review"| A2["🔒 main (internal)"]
84
+
85
+ style E2 fill:#2d3a5a,stroke:#48f,color:#fff
86
+ style F fill:#1a3a1a,stroke:#4a4,color:#ccc
87
+ style A2 fill:#2d5a2d,stroke:#4a4,color:#fff
88
+ ```
89
+
90
+ </td>
91
+ </tr>
92
+ </table>
93
+
94
+ ## Getting Started
95
+
96
+ ### Prerequisites
97
+
98
+ - Python 3.10+ and `git` CLI
99
+ - An existing internal repo with an `origin` remote
100
+ - An existing public repo with at least one commit (e.g. a README created during repo setup)
101
+ - *(Optional)* [`gh` CLI](https://cli.github.com/) authenticated via `gh auth login`. Enables automatic PR creation for GitHub-hosted repos. Without it, pubgate logs the manual steps instead.
102
+ - *(Optional)* [`az` CLI](https://learn.microsoft.com/en-us/cli/azure/) with the `azure-devops` extension, authenticated via `az login`. Enables automatic PR creation for Azure DevOps-hosted repos. The extension is installed automatically if missing. Without it, pubgate logs the manual steps instead.
103
+ - *(Optional)* [Git LFS](https://git-lfs.com/) if your repo uses LFS-tracked files. pubgate auto-detects LFS and handles pointer files automatically. Without it, LFS-specific operations are silently skipped.
104
+ - A clean worktree on `main`, synced with `origin` (no uncommitted changes, no unpushed commits)
105
+
106
+ ### Setup
107
+
108
+ 1. Install:
109
+ ```bash
110
+ pip install pubgate
111
+ ```
112
+ 2. Create `pubgate.toml` in repo root:
113
+ ```toml
114
+ public_url = "https://github.com/you/public-repo.git"
115
+ ```
116
+ Built-in ignore patterns cover common conventions (`.internal/*`, `internal/*`, `*-internal.*`, `*.internal.*`, `*_internal.*`, `*-private.*`, `*.private.*`, `*_private.*`, `*.secret`, `*.secrets`). To override them, set `ignore` explicitly (see [Configuration](#configuration)).
117
+ 3. Optionally, mark internal-only sections in files (in addition to ignore patterns, you can hide parts of individual files). Three comment styles are supported:
118
+ ```python
119
+ # BEGIN-INTERNAL
120
+ secret_stuff()
121
+ # END-INTERNAL
122
+ ```
123
+ ```javascript
124
+ // BEGIN-INTERNAL
125
+ secretStuff();
126
+ // END-INTERNAL
127
+ ```
128
+ ```html
129
+ <!-- BEGIN-INTERNAL -->
130
+ <div class="secret">...</div>
131
+ <!-- END-INTERNAL -->
132
+ ```
133
+ Markers must be properly paired. Nested, unclosed, or orphan `END-INTERNAL` markers cause an error. After scrubbing, a residual check catches any surviving markers that were not removed.
134
+ 4. Commit and push your changes to `main` (direct push or via PR). `pubgate absorb` requires a clean worktree synced with `origin`.
135
+ 5. Initialize tracking:
136
+ ```bash
137
+ pubgate absorb
138
+ ```
139
+ On first run, this records the current public repo HEAD as the starting point for future syncs. It creates a PR branch that records this baseline in a tracking file (`.pubgate-absorbed`). Create a PR from that branch into `main` on your git host and merge it.
140
+ 6. After your first `pubgate stage` run creates the `pubgate/public-approved` branch, protect it on your git host: require pull requests (no direct pushes) and optionally require approvals. This ensures content only reaches the approved branch through reviewed PRs — the leak-review gate.
141
+
142
+ ## Workflow
143
+
144
+ ### Making changes public: absorb → stage → publish
145
+
146
+ pubgate prepares branches for review. When a supported CLI is available (`gh` for GitHub, `az` for Azure DevOps) and the remote is a recognized host, pubgate automatically creates or updates the PR for each branch it pushes. Otherwise it logs the manual steps. Use `--no-pr` to skip automatic PR creation. After merging changes into internal `main` through your normal PR process:
147
+
148
+ 1. Recommended: run `pubgate absorb` if the public repo has unabsorbed changes, then merge the absorb PR. This isn't required (`stage` and `publish` proceed either way) but it keeps the public snapshot clean.
149
+ 2. Run `pubgate stage`. This creates a `pubgate/stage` branch with the filtered snapshot for leak review.
150
+ 3. Review the PR from `pubgate/stage` → `pubgate/public-approved` (created automatically on GitHub/Azure DevOps, or create it manually on other hosts). This is the leak-review gate. Review it to ensure no internal code is exposed. Merge when satisfied.
151
+ 4. Run `pubgate publish`. This delivers the reviewed content to the public repo as a `pubgate/publish` branch.
152
+ 5. Review the PR from `pubgate/publish` → `main` on the public repo (created automatically on GitHub/Azure DevOps, or create it manually). Merge after CI passes.
153
+
154
+ ### Incorporating public contributions: absorb
155
+
156
+ Run when the public repo has external contributions that need to be brought into the internal repo.
157
+
158
+ 1. Run `pubgate absorb`. This creates a `pubgate/absorb` branch with the merged public changes for review.
159
+ 2. Review the PR from `pubgate/absorb` → `main` (created automatically on GitHub/Azure DevOps, or create it manually on other hosts).
160
+ 3. Resolve conflicts if any.
161
+ 4. Merge the PR.
162
+
163
+ ## Branches and State Tracking
164
+
165
+ **Branches**
166
+
167
+ Making changes public (absorb → stage → publish):
168
+ - **`main`** (internal): internal development branch (protected)
169
+ - **`pubgate/stage`** (internal): branch for leak review: filtered internal content → `pubgate/public-approved`
170
+ - **`pubgate/public-approved`** (internal): holds reviewed staged content approved for publication; created automatically on first `stage` if it doesn't exist. **Protect this branch** on your git host — require PRs (no direct pushes), and optionally require approvals. This is the leak-review gate: only content that passes PR review should land here.
171
+ - **`pubgate/publish`** (public): branch for publish review: reviewed content → public `main`
172
+ - **`main`** (public): public-facing branch (protected)
173
+
174
+ Incorporating public contributions (absorb):
175
+ - **`pubgate/absorb`** (internal): branch for merge review: public changes → internal `main`
176
+
177
+ **State files**
178
+
179
+ - `.pubgate-absorbed` (on `main`): tracks which public commit was last absorbed
180
+ - `.pubgate-staged` (on `pubgate/public-approved`): tracks which internal commit was last staged
181
+
182
+ Created and updated automatically.
183
+
184
+ ## CLI Reference
185
+
186
+ | Command | What it does |
187
+ |---------|-------------|
188
+ | `pubgate stage` | Build a filtered snapshot of internal code and create a branch for leak review |
189
+ | `pubgate publish` | Push reviewed content to a PR branch on the public repo |
190
+ | `pubgate absorb` | Merge public contributions into an internal branch for review |
191
+ | `pubgate status` | Show sync status of absorb, stage, and publish (read-only, fetches remotes) |
192
+
193
+ Flags `--dry-run`, `--force`, and `--no-pr` apply to `absorb`, `stage`, and `publish` (not `status`). Flags come after the command; `--repo-dir` comes before it.
194
+
195
+ | Flag | Position | Description |
196
+ |------|----------|-------------|
197
+ | `--dry-run` | after command | Show planned actions without writing branches or files. Still syncs with remotes to ensure accurate plans. Example: `pubgate stage --dry-run` |
198
+ | `--force` | after command | Overwrite an existing PR branch from a previous run whose PR was not yet merged. Without this flag, pubgate errors out if the PR branch already exists. Force-push is blocked on protected branches (`main`, `pubgate/public-approved`, and public `main`). Example: `pubgate absorb --force` |
199
+ | `--no-pr` | after command | Skip automatic PR creation even when a supported CLI (`gh`/`az`) is available. pubgate will still push the branch and log manual steps. Example: `pubgate stage --no-pr` |
200
+ | `--repo-dir` | before command | Run pubgate against a specific repo path instead of the current directory. Example: `pubgate --repo-dir /path/to/repo stage` |
201
+
202
+ ## Configuration
203
+
204
+ Full `pubgate.toml` example (all fields shown with defaults, only `public_url` is required for first-time setup when the remote doesn't already exist):
205
+
206
+ ```toml
207
+ # Internal repo
208
+ internal_main_branch = "main"
209
+ internal_approved_branch = "pubgate/public-approved"
210
+ internal_absorb_branch = "pubgate/absorb"
211
+ internal_stage_branch = "pubgate/stage"
212
+
213
+ # Public repo (public_url is required if the git remote isn't already configured)
214
+ public_url = "https://github.com/you/public-repo.git"
215
+ public_remote = "public-remote"
216
+ public_main_branch = "main"
217
+ public_publish_branch = "pubgate/publish"
218
+
219
+ # State tracking
220
+ absorb_state_file = ".pubgate-absorbed"
221
+ stage_state_file = ".pubgate-staged"
222
+
223
+ # Filtering (fnmatch syntax; patterns match against both full path and basename)
224
+ # These override the built-in defaults. Omit to use the defaults:
225
+ # .internal/* internal/* *-internal.* *.internal.* *_internal.*
226
+ # *-private.* *.private.* *_private.* *.secret *.secrets
227
+ ignore = [
228
+ ".internal/*",
229
+ "*-internal.*",
230
+ "*.internal.*",
231
+ "*.secret",
232
+ ]
233
+ ```
234
+
235
+ ## Edge Cases
236
+
237
+ - **Binary files**: included as-is in staged snapshots (`BEGIN-INTERNAL` markers inside binaries are not processed); during absorb, binary modifications take the public version and are flagged for manual review.
238
+ - **Git LFS files**: LFS pointers pass through all pipelines without modification. LFS files are treated as binary (never merged, never scrubbed for internal markers). pubgate runs `git lfs fetch`/`push` automatically during absorb and publish. Use ignore patterns in `pubgate.toml` to exclude sensitive LFS files from publication. If LFS is not installed, these operations are silently skipped.
239
+ - **Renames on public repo**: the new path is copied in; the old file is kept locally and flagged for review.
240
+ - **Deletions on public repo**: deleted files are kept locally and flagged for review in the absorb PR.
241
+ - **Merge conflicts**: absorb uses three-way merge. Conflicts produce standard git conflict markers (`<<<<<<<`/`=======`/`>>>>>>>`) for manual resolution.
242
+ - **Sync artifacts**: absorb excludes both state files (`.pubgate-absorbed`, `.pubgate-staged`) from the diff (they are sync artifacts, not external contributions). When only state files changed since the last absorb, the resulting PR only updates `.pubgate-absorbed` (tracking-only).
243
+ - **Empty files after scrubbing**: files that become empty after removing `BEGIN-INTERNAL` blocks are still included in the staged snapshot.
244
+ - **External contribution between stage and publish**: if someone pushes to the public repo after you stage but before you publish, `publish` still proceeds: it bases the public PR on the last absorbed commit, and git's three-way merge preserves external contributions or surfaces conflicts in the public PR. For a clean snapshot, run `absorb` → merge absorb PR → `stage` → merge stage PR → `publish`.
245
+ - **Stale branch cleanup**: after you merge a PR and its source branch is auto-deleted on the server, pubgate automatically prunes the stale local branch on the next run. No manual cleanup needed.
246
+ - **Commit messages**: absorb commit messages list the public commits being absorbed (safe, they are already public). Stage commit messages list the internal commits since the last stage (safe, stays on the internal repo; useful context for the leak reviewer).
247
+ - **Repeated publish without absorb**: if you publish multiple times without running `absorb` between cycles, each publish PR is based on the same absorbed commit. This produces a trivially resolvable merge conflict on `.pubgate-staged` in the public PR (take the newer value). Running `absorb` between cycles avoids this.
248
+ - **Do not edit the pubgate PR branch directly**: the `pubgate/publish` branch must only contain content produced by `publish`. Manual edits to this branch before merging will be silently overwritten by the next publish cycle (they are not detected as external contributions). If published content needs a fix, make the change in the internal repo and re-run `stage` → `publish`.
249
+
250
+ ## Troubleshooting
251
+
252
+ | Error | Cause | Fix |
253
+ |-------|-------|-----|
254
+ | "working tree is not clean" | Dirty worktree | Commit or stash your changes |
255
+ | "expected branch 'main', currently on '...'" | Not on the main branch | Run `git checkout main` |
256
+ | "HEAD is detached" | Detached HEAD state | Run `git checkout main` |
257
+ | "unpushed commit(s)" | Local `main` is ahead of origin | Push your commits or reset |
258
+ | "behind" | Local `main` is behind origin | Run `git pull --rebase` |
259
+ | "diverged" | Local `main` has diverged from origin | Reconcile manually (rebase or reset) |
260
+ | "branch '...' already exists" | Previous PR not merged | Merge the PR, or use `--force` to overwrite |
261
+ | "no absorb state found" | First run, or absorb not yet done | Run `pubgate absorb` to create initial baseline |
262
+ | "no stage state found" | Stage PR not merged | Run `pubgate stage` and merge the internal PR |
263
+ | "has no 'main' branch" | Public repo is empty (no commits) | Push at least one commit to the public repo (e.g. add a README) |
264
+
265
+ ## Example: Full First-Time Walkthrough
266
+
267
+ ```bash
268
+ # 1. Clone your internal repo and cd into it
269
+ git clone git@internal-host:you/internal-repo.git
270
+ cd internal-repo
271
+
272
+ # 2. Create pubgate.toml (built-in ignore patterns cover common conventions)
273
+ cat > pubgate.toml << 'EOF'
274
+ public_url = "https://github.com/you/public-repo.git"
275
+ EOF
276
+ git add pubgate.toml && git commit -m "Add pubgate config" && git push
277
+
278
+ # 3. Bootstrap - records the public repo's current HEAD as baseline
279
+ pubgate absorb
280
+ # Output: pushes pubgate/absorb branch
281
+ # → If gh/az CLI is set up, a PR is created automatically
282
+ # → Otherwise, go to your git host, create PR: pubgate/absorb → main
283
+ # → Merge the PR
284
+
285
+ # 4. Stage staged content (filters out internal files and scrubs markers)
286
+ pubgate stage
287
+ # Output: pushes pubgate/stage branch
288
+ # → PR: pubgate/stage → pubgate/public-approved (auto-created on GitHub/Azure DevOps)
289
+ # → Review for leaks, merge it
290
+
291
+ # 5. Publish to public repo
292
+ pubgate publish
293
+ # Output: pushes pubgate/publish to the public remote
294
+ # → PR: pubgate/publish → main on the public repo (auto-created on GitHub/Azure DevOps)
295
+ # → Merge after CI passes
296
+
297
+ # Done! For future syncs: absorb (if needed) → stage → publish.
298
+ ```
299
+
300
+ ## Development
301
+
302
+ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
303
+
304
+ For design decisions and detailed command specifications, see [SPEC.md](SPEC.md).
305
+
306
+ ```bash
307
+ uv sync # install dependencies
308
+ uv run pre-commit install # set up pre-commit hooks (ruff, ty, etc.)
309
+ uv run pytest # run tests (-n auto for parallel)
310
+ uv run pre-commit run -a # run all linting & formatting checks
311
+ ```
312
+
313
+ Tests create temporary git repos locally. No network access needed.
314
+
315
+ ## License
316
+
317
+ MIT. See [LICENSE](LICENSE).