git-sync-filtered 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.
- git_sync_filtered-0.1.0/LICENSE +21 -0
- git_sync_filtered-0.1.0/PKG-INFO +130 -0
- git_sync_filtered-0.1.0/README.md +104 -0
- git_sync_filtered-0.1.0/git_sync_filtered/__main__.py +12 -0
- git_sync_filtered-0.1.0/git_sync_filtered/cli.py +77 -0
- git_sync_filtered-0.1.0/git_sync_filtered/sync.py +128 -0
- git_sync_filtered-0.1.0/git_sync_filtered.egg-info/PKG-INFO +130 -0
- git_sync_filtered-0.1.0/git_sync_filtered.egg-info/SOURCES.txt +12 -0
- git_sync_filtered-0.1.0/git_sync_filtered.egg-info/dependency_links.txt +1 -0
- git_sync_filtered-0.1.0/git_sync_filtered.egg-info/entry_points.txt +2 -0
- git_sync_filtered-0.1.0/git_sync_filtered.egg-info/requires.txt +8 -0
- git_sync_filtered-0.1.0/git_sync_filtered.egg-info/top_level.txt +1 -0
- git_sync_filtered-0.1.0/pyproject.toml +44 -0
- git_sync_filtered-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Merge 42
|
|
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,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-sync-filtered
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sync filtered commits from private to public repository
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: git,filter,sync,repo
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: click>=8.0
|
|
19
|
+
Requires-Dist: gitpython>=3.1
|
|
20
|
+
Requires-Dist: git-filter-repo>=2.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff; extra == "dev"
|
|
24
|
+
Requires-Dist: mypy; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# git-sync-filtered
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+

|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
A thin wrapper around [git-filter-repo](https://github.com/newren/git-filter-repo) for syncing filtered commits from a private repository to a public repository.
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
git-sync-filtered clones a private repository, filters it to only include specified paths using git-filter-repo, and pushes the result to a public repository's sync branch. This enables maintaining a public subset of a private repository while preserving commit history.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
### uv (recommended)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install git+https://github.com/Merge-42/git-sync-filtered
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### uvx (run without installing)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uvx git+https://github.com/Merge-42/git-sync-filtered \
|
|
51
|
+
--private git@github.com:org/private.git \
|
|
52
|
+
--public git@github.com:org/public.git \
|
|
53
|
+
--keep src \
|
|
54
|
+
--keep docs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### pip
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install git+https://github.com/Merge-42/git-sync-filtered
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git-sync-filtered \
|
|
67
|
+
--private git@github.com:org/private.git \
|
|
68
|
+
--public git@github.com:org/public.git \
|
|
69
|
+
--keep src \
|
|
70
|
+
--keep docs
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Using a paths file
|
|
74
|
+
|
|
75
|
+
Create a file with paths to keep (one per line, lines starting with `#` are comments):
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
src
|
|
79
|
+
docs
|
|
80
|
+
README.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git-sync-filtered \
|
|
85
|
+
--private git@github.com:org/private.git \
|
|
86
|
+
--public git@github.com:org/public.git \
|
|
87
|
+
--keep-from-file paths.txt
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
| Option | Description | Default |
|
|
93
|
+
| ------------------ | --------------------------------------------- | --------------- |
|
|
94
|
+
| `--private` | Private repo path or URL | Required |
|
|
95
|
+
| `--public` | Public repo path or URL | Required |
|
|
96
|
+
| `--keep` | Paths to keep (specify multiple) | Required |
|
|
97
|
+
| `--keep-from-file` | File containing paths to keep | - |
|
|
98
|
+
| `--sync-branch` | Sync branch name | `upstream/sync` |
|
|
99
|
+
| `--main-branch` | Main branch name | `main` |
|
|
100
|
+
| `--private-branch` | Private branch to sync from | `main` |
|
|
101
|
+
| `--dry-run` | Show what would happen without making changes | `false` |
|
|
102
|
+
| `--merge` | Merge into main branch after sync | `false` |
|
|
103
|
+
| `--force` | Force push | `false` |
|
|
104
|
+
|
|
105
|
+
## How It Works
|
|
106
|
+
|
|
107
|
+
1. Clones the private repository
|
|
108
|
+
2. Runs git-filter-repo to filter to only the specified paths
|
|
109
|
+
3. Pushes filtered commits to the public repository's sync branch
|
|
110
|
+
4. Optionally merges the sync branch into main
|
|
111
|
+
|
|
112
|
+
The sync branch can then be merged into main manually or with `--merge`.
|
|
113
|
+
|
|
114
|
+
## Workflow
|
|
115
|
+
|
|
116
|
+
```mermaid
|
|
117
|
+
flowchart LR
|
|
118
|
+
A[Private Repo<br/>private-branch] -->|clone & filter| B[git-sync-filtered]
|
|
119
|
+
B -->|push| C[Public Repo<br/>sync-branch]
|
|
120
|
+
C -->|"merge<br/>(manual or with --merge)"| D[Public Repo<br/>main-branch]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
|
|
125
|
+
- Python 3.10+
|
|
126
|
+
- git >= 2.36.0
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
Sponsored by [Merge 42](https://merge42.com)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# git-sync-filtered
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
A thin wrapper around [git-filter-repo](https://github.com/newren/git-filter-repo) for syncing filtered commits from a private repository to a public repository.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
git-sync-filtered clones a private repository, filters it to only include specified paths using git-filter-repo, and pushes the result to a public repository's sync branch. This enables maintaining a public subset of a private repository while preserving commit history.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### uv (recommended)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install git+https://github.com/Merge-42/git-sync-filtered
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### uvx (run without installing)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uvx git+https://github.com/Merge-42/git-sync-filtered \
|
|
25
|
+
--private git@github.com:org/private.git \
|
|
26
|
+
--public git@github.com:org/public.git \
|
|
27
|
+
--keep src \
|
|
28
|
+
--keep docs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### pip
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install git+https://github.com/Merge-42/git-sync-filtered
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git-sync-filtered \
|
|
41
|
+
--private git@github.com:org/private.git \
|
|
42
|
+
--public git@github.com:org/public.git \
|
|
43
|
+
--keep src \
|
|
44
|
+
--keep docs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Using a paths file
|
|
48
|
+
|
|
49
|
+
Create a file with paths to keep (one per line, lines starting with `#` are comments):
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
src
|
|
53
|
+
docs
|
|
54
|
+
README.md
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git-sync-filtered \
|
|
59
|
+
--private git@github.com:org/private.git \
|
|
60
|
+
--public git@github.com:org/public.git \
|
|
61
|
+
--keep-from-file paths.txt
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Options
|
|
65
|
+
|
|
66
|
+
| Option | Description | Default |
|
|
67
|
+
| ------------------ | --------------------------------------------- | --------------- |
|
|
68
|
+
| `--private` | Private repo path or URL | Required |
|
|
69
|
+
| `--public` | Public repo path or URL | Required |
|
|
70
|
+
| `--keep` | Paths to keep (specify multiple) | Required |
|
|
71
|
+
| `--keep-from-file` | File containing paths to keep | - |
|
|
72
|
+
| `--sync-branch` | Sync branch name | `upstream/sync` |
|
|
73
|
+
| `--main-branch` | Main branch name | `main` |
|
|
74
|
+
| `--private-branch` | Private branch to sync from | `main` |
|
|
75
|
+
| `--dry-run` | Show what would happen without making changes | `false` |
|
|
76
|
+
| `--merge` | Merge into main branch after sync | `false` |
|
|
77
|
+
| `--force` | Force push | `false` |
|
|
78
|
+
|
|
79
|
+
## How It Works
|
|
80
|
+
|
|
81
|
+
1. Clones the private repository
|
|
82
|
+
2. Runs git-filter-repo to filter to only the specified paths
|
|
83
|
+
3. Pushes filtered commits to the public repository's sync branch
|
|
84
|
+
4. Optionally merges the sync branch into main
|
|
85
|
+
|
|
86
|
+
The sync branch can then be merged into main manually or with `--merge`.
|
|
87
|
+
|
|
88
|
+
## Workflow
|
|
89
|
+
|
|
90
|
+
```mermaid
|
|
91
|
+
flowchart LR
|
|
92
|
+
A[Private Repo<br/>private-branch] -->|clone & filter| B[git-sync-filtered]
|
|
93
|
+
B -->|push| C[Public Repo<br/>sync-branch]
|
|
94
|
+
C -->|"merge<br/>(manual or with --merge)"| D[Public Repo<br/>main-branch]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
- Python 3.10+
|
|
100
|
+
- git >= 2.36.0
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Sponsored by [Merge 42](https://merge42.com)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
git-sync-filtered - Sync filtered commits from private to public repo
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
git-sync-filtered --private /path/to/private --public /path/to/public --keep src --keep docs
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from git_sync_filtered.cli import main
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from git_sync_filtered.sync import sync
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
@click.option("--private", required=True, help="Private repo path or URL")
|
|
8
|
+
@click.option("--public", required=True, help="Public repo path or URL")
|
|
9
|
+
@click.option("--keep", multiple=True, help="Paths to keep (can specify multiple)")
|
|
10
|
+
@click.option(
|
|
11
|
+
"--keep-from-file",
|
|
12
|
+
type=click.Path(exists=True),
|
|
13
|
+
help="File containing paths to keep (one per line)",
|
|
14
|
+
)
|
|
15
|
+
@click.option("--sync-branch", default="upstream/sync", help="Sync branch name")
|
|
16
|
+
@click.option("--main-branch", default="main", help="Main branch name")
|
|
17
|
+
@click.option("--private-branch", default="main", help="Private branch to sync from")
|
|
18
|
+
@click.option(
|
|
19
|
+
"--dry-run", is_flag=True, help="Show what would happen without making changes"
|
|
20
|
+
)
|
|
21
|
+
@click.option("--merge", is_flag=True, help="Merge into main branch after sync")
|
|
22
|
+
@click.option("--force", is_flag=True, help="Force push")
|
|
23
|
+
def main(
|
|
24
|
+
private,
|
|
25
|
+
public,
|
|
26
|
+
keep,
|
|
27
|
+
keep_from_file,
|
|
28
|
+
sync_branch,
|
|
29
|
+
main_branch,
|
|
30
|
+
private_branch,
|
|
31
|
+
dry_run,
|
|
32
|
+
merge,
|
|
33
|
+
force,
|
|
34
|
+
):
|
|
35
|
+
"""Sync filtered commits from private to public repository."""
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
result = sync(
|
|
39
|
+
private=private,
|
|
40
|
+
public=public,
|
|
41
|
+
keep=keep,
|
|
42
|
+
keep_from_file=keep_from_file,
|
|
43
|
+
sync_branch=sync_branch,
|
|
44
|
+
main_branch=main_branch,
|
|
45
|
+
private_branch=private_branch,
|
|
46
|
+
dry_run=dry_run,
|
|
47
|
+
merge=merge,
|
|
48
|
+
force=force,
|
|
49
|
+
)
|
|
50
|
+
except ValueError as e:
|
|
51
|
+
raise click.ClickException(str(e))
|
|
52
|
+
|
|
53
|
+
click.echo("=== Git Filter Sync ===")
|
|
54
|
+
click.echo(f"Private: {private}")
|
|
55
|
+
click.echo(f"Public: {public}")
|
|
56
|
+
click.echo(f"Keep: {result['paths_to_keep']}")
|
|
57
|
+
click.echo(f"Sync to: {sync_branch}")
|
|
58
|
+
click.echo()
|
|
59
|
+
|
|
60
|
+
if dry_run:
|
|
61
|
+
click.echo(f"[git-sync] DRY RUN - Would push to {sync_branch}")
|
|
62
|
+
click.echo()
|
|
63
|
+
click.echo("Commits that would be pushed:")
|
|
64
|
+
for commit in result["dry_run_commits"]:
|
|
65
|
+
click.echo(commit)
|
|
66
|
+
else:
|
|
67
|
+
click.echo()
|
|
68
|
+
click.echo(f"=== Synced to {sync_branch} ===")
|
|
69
|
+
|
|
70
|
+
if merge and not dry_run:
|
|
71
|
+
if result["merge_success"]:
|
|
72
|
+
click.echo(f"[git-sync] Merged and pushed to {main_branch}")
|
|
73
|
+
else:
|
|
74
|
+
click.echo("[git-sync] Merge conflict! Please resolve manually:")
|
|
75
|
+
|
|
76
|
+
click.echo()
|
|
77
|
+
click.echo("Done!")
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from itertools import filterfalse
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from tempfile import TemporaryDirectory
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import git
|
|
8
|
+
from git_filter_repo import FilteringOptions, RepoFilter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_paths_from_file(path: Path) -> list[str]:
|
|
12
|
+
lines = (line.strip() for line in path.read_text().splitlines())
|
|
13
|
+
return list(filterfalse(lambda line: line.startswith("#") or not line, lines))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def collect_paths_to_keep(
|
|
17
|
+
keep: tuple[str, ...], keep_from_file: Optional[str]
|
|
18
|
+
) -> list[str]:
|
|
19
|
+
paths_to_keep = set(keep)
|
|
20
|
+
|
|
21
|
+
if keep_from_file:
|
|
22
|
+
paths_to_keep.update(read_paths_from_file(Path(keep_from_file)))
|
|
23
|
+
|
|
24
|
+
return sorted(list(paths_to_keep))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_filter_repo(repo_path: str, paths_to_keep: list[str]) -> None:
|
|
28
|
+
old_cwd = os.getcwd()
|
|
29
|
+
os.chdir(repo_path)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
argv = ["--force", "--partial"]
|
|
33
|
+
for path in paths_to_keep:
|
|
34
|
+
argv.extend(["--path", path])
|
|
35
|
+
|
|
36
|
+
filter_args = FilteringOptions.parse_args(argv, error_on_empty=False)
|
|
37
|
+
repo_filter = RepoFilter(filter_args)
|
|
38
|
+
repo_filter.run()
|
|
39
|
+
finally:
|
|
40
|
+
os.chdir(old_cwd)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def push_to_remote(
|
|
44
|
+
repo: git.Repo,
|
|
45
|
+
public_url: str,
|
|
46
|
+
sync_branch: str,
|
|
47
|
+
private_branch: str,
|
|
48
|
+
force: bool = False,
|
|
49
|
+
dry_run: bool = False,
|
|
50
|
+
) -> list[str]:
|
|
51
|
+
if "public" not in repo.remotes:
|
|
52
|
+
repo.create_remote("public", public_url)
|
|
53
|
+
else:
|
|
54
|
+
repo.remote("public").set_url(public_url)
|
|
55
|
+
|
|
56
|
+
repo.remote("public").fetch()
|
|
57
|
+
|
|
58
|
+
if dry_run:
|
|
59
|
+
commits = []
|
|
60
|
+
for commit in repo.iter_commits(private_branch):
|
|
61
|
+
commits.append(f" {commit.hexsha[:8]} {commit.summary}")
|
|
62
|
+
return commits
|
|
63
|
+
else:
|
|
64
|
+
refspec = f"refs/heads/{private_branch}:refs/heads/{sync_branch}"
|
|
65
|
+
repo.remote("public").push(refspec=refspec, force=force)
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def merge_into_main(
|
|
70
|
+
repo: git.Repo,
|
|
71
|
+
main_branch: str,
|
|
72
|
+
sync_branch: str,
|
|
73
|
+
) -> bool:
|
|
74
|
+
repo.heads[main_branch].checkout()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
sync_head = repo.heads[sync_branch]
|
|
78
|
+
repo.index.merge_commit(sync_head, msg=f"Merge branch '{sync_branch}'")
|
|
79
|
+
|
|
80
|
+
repo.remote("public").push(
|
|
81
|
+
refspec=f"refs/heads/{main_branch}:refs/heads/{main_branch}"
|
|
82
|
+
)
|
|
83
|
+
return True
|
|
84
|
+
except git.GitCommandError:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sync(
|
|
89
|
+
private: str,
|
|
90
|
+
public: str,
|
|
91
|
+
keep: tuple[str, ...],
|
|
92
|
+
keep_from_file: Optional[str],
|
|
93
|
+
sync_branch: str,
|
|
94
|
+
main_branch: str,
|
|
95
|
+
private_branch: str,
|
|
96
|
+
dry_run: bool,
|
|
97
|
+
merge: bool,
|
|
98
|
+
force: bool,
|
|
99
|
+
):
|
|
100
|
+
paths_to_keep = collect_paths_to_keep(keep, keep_from_file)
|
|
101
|
+
|
|
102
|
+
if not paths_to_keep:
|
|
103
|
+
raise ValueError("At least one --keep path or --keep-from-file required")
|
|
104
|
+
|
|
105
|
+
with TemporaryDirectory(prefix="git-sync-") as work_dir:
|
|
106
|
+
work_dir_path = Path(work_dir)
|
|
107
|
+
private_clone = work_dir_path / "private"
|
|
108
|
+
private_repo = git.Repo.clone_from(private, str(private_clone))
|
|
109
|
+
|
|
110
|
+
run_filter_repo(str(private_clone), paths_to_keep)
|
|
111
|
+
|
|
112
|
+
dry_run_commits = push_to_remote(
|
|
113
|
+
private_repo, public, sync_branch, private_branch, force, dry_run
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if merge and not dry_run:
|
|
117
|
+
success = merge_into_main(private_repo, main_branch, sync_branch)
|
|
118
|
+
return {
|
|
119
|
+
"paths_to_keep": paths_to_keep,
|
|
120
|
+
"dry_run_commits": dry_run_commits,
|
|
121
|
+
"merge_success": success,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"paths_to_keep": paths_to_keep,
|
|
126
|
+
"dry_run_commits": dry_run_commits,
|
|
127
|
+
"merge_success": None,
|
|
128
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-sync-filtered
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sync filtered commits from private to public repository
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: git,filter,sync,repo
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: click>=8.0
|
|
19
|
+
Requires-Dist: gitpython>=3.1
|
|
20
|
+
Requires-Dist: git-filter-repo>=2.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff; extra == "dev"
|
|
24
|
+
Requires-Dist: mypy; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# git-sync-filtered
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+

|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
A thin wrapper around [git-filter-repo](https://github.com/newren/git-filter-repo) for syncing filtered commits from a private repository to a public repository.
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
git-sync-filtered clones a private repository, filters it to only include specified paths using git-filter-repo, and pushes the result to a public repository's sync branch. This enables maintaining a public subset of a private repository while preserving commit history.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
### uv (recommended)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install git+https://github.com/Merge-42/git-sync-filtered
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### uvx (run without installing)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uvx git+https://github.com/Merge-42/git-sync-filtered \
|
|
51
|
+
--private git@github.com:org/private.git \
|
|
52
|
+
--public git@github.com:org/public.git \
|
|
53
|
+
--keep src \
|
|
54
|
+
--keep docs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### pip
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install git+https://github.com/Merge-42/git-sync-filtered
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git-sync-filtered \
|
|
67
|
+
--private git@github.com:org/private.git \
|
|
68
|
+
--public git@github.com:org/public.git \
|
|
69
|
+
--keep src \
|
|
70
|
+
--keep docs
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Using a paths file
|
|
74
|
+
|
|
75
|
+
Create a file with paths to keep (one per line, lines starting with `#` are comments):
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
src
|
|
79
|
+
docs
|
|
80
|
+
README.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git-sync-filtered \
|
|
85
|
+
--private git@github.com:org/private.git \
|
|
86
|
+
--public git@github.com:org/public.git \
|
|
87
|
+
--keep-from-file paths.txt
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
| Option | Description | Default |
|
|
93
|
+
| ------------------ | --------------------------------------------- | --------------- |
|
|
94
|
+
| `--private` | Private repo path or URL | Required |
|
|
95
|
+
| `--public` | Public repo path or URL | Required |
|
|
96
|
+
| `--keep` | Paths to keep (specify multiple) | Required |
|
|
97
|
+
| `--keep-from-file` | File containing paths to keep | - |
|
|
98
|
+
| `--sync-branch` | Sync branch name | `upstream/sync` |
|
|
99
|
+
| `--main-branch` | Main branch name | `main` |
|
|
100
|
+
| `--private-branch` | Private branch to sync from | `main` |
|
|
101
|
+
| `--dry-run` | Show what would happen without making changes | `false` |
|
|
102
|
+
| `--merge` | Merge into main branch after sync | `false` |
|
|
103
|
+
| `--force` | Force push | `false` |
|
|
104
|
+
|
|
105
|
+
## How It Works
|
|
106
|
+
|
|
107
|
+
1. Clones the private repository
|
|
108
|
+
2. Runs git-filter-repo to filter to only the specified paths
|
|
109
|
+
3. Pushes filtered commits to the public repository's sync branch
|
|
110
|
+
4. Optionally merges the sync branch into main
|
|
111
|
+
|
|
112
|
+
The sync branch can then be merged into main manually or with `--merge`.
|
|
113
|
+
|
|
114
|
+
## Workflow
|
|
115
|
+
|
|
116
|
+
```mermaid
|
|
117
|
+
flowchart LR
|
|
118
|
+
A[Private Repo<br/>private-branch] -->|clone & filter| B[git-sync-filtered]
|
|
119
|
+
B -->|push| C[Public Repo<br/>sync-branch]
|
|
120
|
+
C -->|"merge<br/>(manual or with --merge)"| D[Public Repo<br/>main-branch]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
|
|
125
|
+
- Python 3.10+
|
|
126
|
+
- git >= 2.36.0
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
Sponsored by [Merge 42](https://merge42.com)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
git_sync_filtered/__main__.py
|
|
5
|
+
git_sync_filtered/cli.py
|
|
6
|
+
git_sync_filtered/sync.py
|
|
7
|
+
git_sync_filtered.egg-info/PKG-INFO
|
|
8
|
+
git_sync_filtered.egg-info/SOURCES.txt
|
|
9
|
+
git_sync_filtered.egg-info/dependency_links.txt
|
|
10
|
+
git_sync_filtered.egg-info/entry_points.txt
|
|
11
|
+
git_sync_filtered.egg-info/requires.txt
|
|
12
|
+
git_sync_filtered.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
git_sync_filtered
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "git-sync-filtered"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Sync filtered commits from private to public repository"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Your Name", email = "you@example.com" }]
|
|
13
|
+
keywords = ["git", "filter", "sync", "repo"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
dependencies = ["click>=8.0", "gitpython>=3.1", "git-filter-repo>=2.0"]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest", "ruff", "mypy"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
git-sync-filtered = "git_sync_filtered.__main__:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["."]
|
|
34
|
+
include = ["git_sync_filtered*"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py310"
|
|
39
|
+
|
|
40
|
+
[tool.mypy]
|
|
41
|
+
python_version = "3.10"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = ["pytest>=9.0.2"]
|