polygit 0.1.0__py3-none-any.whl

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.
polygit/__init__.py ADDED
File without changes
polygit/cli.py ADDED
@@ -0,0 +1,57 @@
1
+ import sys
2
+ import argparse
3
+ from .config import find_config, load_config
4
+ from .repo import cmd_sync, cmd_status, cmd_pull, cmd_push
5
+ from .repos import cmd_repos
6
+
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(
10
+ prog="pgit",
11
+ description="Manage multiple git repos defined in a .pgit config file."
12
+ )
13
+ sub = parser.add_subparsers(dest="command", metavar="command")
14
+
15
+ sub.add_parser("sync", help="Clone missing repos and fetch all")
16
+ sub.add_parser("status", help="Show repos with uncommitted changes")
17
+ sub.add_parser("pull", help="Pull all repos, auto-stashing local changes")
18
+
19
+ push_p = sub.add_parser("push", help="Commit and push dirty repos")
20
+ mode_g = push_p.add_mutually_exclusive_group()
21
+ mode_g.add_argument("--interactive", dest="push_mode", action="store_const",
22
+ const="interactive", help="Prompt per repo (default)")
23
+ mode_g.add_argument("--all", dest="push_mode", action="store_const",
24
+ const="all", help="One commit message for all dirty repos")
25
+ mode_g.add_argument("--wip", dest="push_mode", action="store_const",
26
+ const="wip", help='Auto-commit with "WIP <timestamp>"')
27
+ push_p.set_defaults(push_mode="interactive")
28
+
29
+ sub.add_parser("repos", help="Discover drift between .pgit and the filesystem")
30
+
31
+ args = parser.parse_args()
32
+
33
+ if not args.command:
34
+ parser.print_help()
35
+ sys.exit(0)
36
+
37
+ config_path = find_config()
38
+ if config_path is None:
39
+ print("Error: no .pgit config file found in this directory or any parent.", file=sys.stderr)
40
+ print("Create a .pgit file in your repos root directory.", file=sys.stderr)
41
+ sys.exit(1)
42
+
43
+ base = config_path.parent
44
+ config = load_config(config_path)
45
+
46
+ commands = {
47
+ "sync": lambda: cmd_sync(config, base),
48
+ "status": lambda: cmd_status(config, base),
49
+ "pull": lambda: cmd_pull(config, base),
50
+ "push": lambda: cmd_push(config, base, mode=args.push_mode),
51
+ "repos": lambda: cmd_repos(config, base, config_path),
52
+ }
53
+ commands[args.command]()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
polygit/config.py ADDED
@@ -0,0 +1,46 @@
1
+ import yaml
2
+ from pathlib import Path
3
+
4
+ HOSTS = {
5
+ "github": "git@github.com",
6
+ "gitlab": "git@gitlab.com",
7
+ }
8
+
9
+ CONFIG_FILE = ".pgit"
10
+
11
+
12
+ def find_config(start=None):
13
+ """Walk up from start (default: cwd) to find a .pgit config file."""
14
+ current = Path(start or Path.cwd())
15
+ for path in [current] + list(current.parents):
16
+ candidate = path / CONFIG_FILE
17
+ if candidate.exists():
18
+ return candidate
19
+ return None
20
+
21
+
22
+ def load_config(path):
23
+ """Load and return the .pgit YAML config."""
24
+ with open(path) as f:
25
+ return yaml.safe_load(f) or {}
26
+
27
+
28
+ def write_config(data, path):
29
+ """Write .pgit config using only stdlib — preserves null and inline-remote styles."""
30
+ def _lines(node, indent):
31
+ prefix = " " * indent
32
+ out = []
33
+ for key, value in node.items():
34
+ if value is None:
35
+ out.append(f"{prefix}{key}:")
36
+ elif isinstance(value, dict) and set(value.keys()) == {"remote"}:
37
+ out.append(f'{prefix}{key}: {{remote: "{value["remote"]}"}}' )
38
+ elif isinstance(value, dict):
39
+ out.append(f"{prefix}{key}:")
40
+ out.extend(_lines(value, indent + 1))
41
+ else:
42
+ out.append(f"{prefix}{key}: {value}")
43
+ return out
44
+
45
+ with open(path, "w") as f:
46
+ f.write("\n".join(_lines(data, 0)) + "\n")
polygit/repo.py ADDED
@@ -0,0 +1,151 @@
1
+ import subprocess
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from .config import HOSTS
5
+
6
+
7
+ def walk(node, prefix):
8
+ """Yield (local_path, remote_path) for every repo in the tree."""
9
+ if node is None:
10
+ yield prefix, prefix
11
+ elif isinstance(node, dict) and set(node.keys()) == {"remote"}:
12
+ yield prefix, node["remote"]
13
+ elif isinstance(node, dict):
14
+ for key, value in node.items():
15
+ yield from walk(value, f"{prefix}/{key}")
16
+
17
+
18
+ def each_repo(config, base):
19
+ """Yield (platform, local_path, remote_path, dir) for all configured repos."""
20
+ for platform, namespaces in config.items():
21
+ if platform not in HOSTS:
22
+ continue
23
+ for ns_key, ns_value in namespaces.items():
24
+ for local_path, remote_path in walk(ns_value, ns_key):
25
+ yield platform, local_path, remote_path, base / platform / local_path
26
+
27
+
28
+ def each_existing_repo(config, base):
29
+ """Yield only repos that are already cloned locally."""
30
+ for platform, local_path, remote_path, d in each_repo(config, base):
31
+ if (d / ".git").exists():
32
+ yield platform, local_path, remote_path, d
33
+
34
+
35
+ def is_dirty(d):
36
+ result = subprocess.run(
37
+ ["git", "-C", str(d), "status", "--porcelain"],
38
+ capture_output=True, text=True
39
+ )
40
+ return bool(result.stdout.strip())
41
+
42
+
43
+ def _commit_and_push(d, label, msg):
44
+ print(f"Committing {label}...")
45
+ subprocess.run(["git", "-C", str(d), "add", "-A"])
46
+ subprocess.run(["git", "-C", str(d), "commit", "-m", msg])
47
+ result = subprocess.run(["git", "-C", str(d), "push"], capture_output=True, text=True)
48
+ if result.returncode != 0:
49
+ print(f" Push failed: {result.stderr.strip()}")
50
+ else:
51
+ print(f" Pushed.")
52
+
53
+
54
+ def cmd_sync(config, base):
55
+ for platform, local_path, remote_path, d in each_repo(config, base):
56
+ url = f"{HOSTS[platform]}:{remote_path}.git"
57
+ if (d / ".git").exists():
58
+ print(f"Fetching {platform}/{local_path}...")
59
+ subprocess.run(["git", "-C", str(d), "fetch", "--all", "-q"])
60
+ else:
61
+ print(f"Cloning {platform}/{local_path}...")
62
+ d.parent.mkdir(parents=True, exist_ok=True)
63
+ subprocess.run(["git", "clone", url, str(d)])
64
+
65
+
66
+ def cmd_status(config, base):
67
+ found_dirty = False
68
+ for platform, local_path, _, d in each_existing_repo(config, base):
69
+ result = subprocess.run(
70
+ ["git", "-C", str(d), "status", "--short"],
71
+ capture_output=True, text=True
72
+ )
73
+ if result.stdout.strip():
74
+ found_dirty = True
75
+ print(f"\n{platform}/{local_path}:")
76
+ for line in result.stdout.strip().splitlines():
77
+ print(f" {line}")
78
+ if not found_dirty:
79
+ print("All repos are clean.")
80
+
81
+
82
+ def cmd_pull(config, base):
83
+ for platform, local_path, _, d in each_existing_repo(config, base):
84
+ label = f"{platform}/{local_path}"
85
+ stashed = False
86
+ if is_dirty(d):
87
+ print(f"Stashing {label}...")
88
+ subprocess.run(["git", "-C", str(d), "stash"], capture_output=True)
89
+ stashed = True
90
+ print(f"Pulling {label}...")
91
+ result = subprocess.run(
92
+ ["git", "-C", str(d), "pull", "--rebase"],
93
+ capture_output=True, text=True
94
+ )
95
+ if result.returncode != 0:
96
+ print(f" Error: {result.stderr.strip()}")
97
+ elif result.stdout.strip() and result.stdout.strip() != "Already up to date.":
98
+ print(f" {result.stdout.strip()}")
99
+ if stashed:
100
+ pop = subprocess.run(
101
+ ["git", "-C", str(d), "stash", "pop"],
102
+ capture_output=True, text=True
103
+ )
104
+ if pop.returncode != 0:
105
+ print(f" Warning: stash pop had conflicts in {label}")
106
+ else:
107
+ print(f" Stash restored.")
108
+
109
+
110
+ def cmd_push(config, base, mode="interactive"):
111
+ dirty = [
112
+ (platform, local_path, d)
113
+ for platform, local_path, _, d in each_existing_repo(config, base)
114
+ if is_dirty(d)
115
+ ]
116
+
117
+ if not dirty:
118
+ print("No dirty repos to commit.")
119
+ return
120
+
121
+ if mode == "wip":
122
+ msg = f"WIP {datetime.now().strftime('%Y-%m-%d %H:%M')}"
123
+ for platform, local_path, d in dirty:
124
+ _commit_and_push(d, f"{platform}/{local_path}", msg)
125
+
126
+ elif mode == "all":
127
+ print("Dirty repos:")
128
+ for platform, local_path, _ in dirty:
129
+ print(f" {platform}/{local_path}")
130
+ msg = input("\nCommit message for all repos (blank to abort): ").strip()
131
+ if not msg:
132
+ print("Aborted.")
133
+ return
134
+ for platform, local_path, d in dirty:
135
+ _commit_and_push(d, f"{platform}/{local_path}", msg)
136
+
137
+ else: # interactive
138
+ for platform, local_path, d in dirty:
139
+ label = f"{platform}/{local_path}"
140
+ status = subprocess.run(
141
+ ["git", "-C", str(d), "status", "--short"],
142
+ capture_output=True, text=True
143
+ ).stdout.strip()
144
+ print(f"\n{label}:")
145
+ for line in status.splitlines():
146
+ print(f" {line}")
147
+ msg = input(" Commit message (blank to skip): ").strip()
148
+ if not msg:
149
+ print(" Skipped.")
150
+ continue
151
+ _commit_and_push(d, label, msg)
polygit/repos.py ADDED
@@ -0,0 +1,204 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from .config import HOSTS, write_config
4
+ from .repo import walk
5
+
6
+
7
+ def parse_remote_url(url):
8
+ """Parse a git remote URL into (platform, remote_path).
9
+
10
+ git@github.com:org/repo.git -> (github, org/repo)
11
+ git@gitlab.com:ns/sub/repo.git -> (gitlab, ns/sub/repo)
12
+ https://github.com/org/repo.git -> (github, org/repo)
13
+ """
14
+ if not url:
15
+ return None, None
16
+ url = url.strip()
17
+ if url.endswith(".git"):
18
+ url = url[:-4]
19
+ if url.startswith("git@"):
20
+ host, path = url[4:].split(":", 1)
21
+ elif "://" in url:
22
+ rest = url.split("://", 1)[1]
23
+ host, path = rest.split("/", 1)
24
+ else:
25
+ return None, None
26
+ platform = "github" if "github" in host else ("gitlab" if "gitlab" in host else None)
27
+ return platform, path
28
+
29
+
30
+ def del_nested(d, keys):
31
+ """Delete d[k0][k1]...[kn], pruning empty parent dicts."""
32
+ if not keys or keys[0] not in d:
33
+ return
34
+ if len(keys) == 1:
35
+ del d[keys[0]]
36
+ else:
37
+ del_nested(d[keys[0]], keys[1:])
38
+ if isinstance(d[keys[0]], dict) and not d[keys[0]]:
39
+ del d[keys[0]]
40
+
41
+
42
+ def _add_to_config(config, fs_platform, local_path, remote_url):
43
+ """Insert a repo entry into the config data structure."""
44
+ local_parts = local_path.split("/")
45
+ local_name = local_parts[-1]
46
+ _, remote_path = parse_remote_url(remote_url)
47
+ platform_data = config.setdefault(fs_platform, {})
48
+
49
+ if remote_path:
50
+ r_parts = remote_path.split("/")
51
+ r_name = r_parts[-1]
52
+ r_ns = "/".join(r_parts[:-1])
53
+ ns_data = platform_data.setdefault(r_ns, {})
54
+ if local_path == f"{r_ns}/{r_name}":
55
+ ns_data[r_name] = None
56
+ else:
57
+ sub = local_path[len(r_ns)+1:] if local_path.startswith(r_ns + "/") else local_name
58
+ node = ns_data
59
+ for part in sub.split("/")[:-1]:
60
+ node = node.setdefault(part, {})
61
+ node[sub.split("/")[-1]] = {"remote": remote_path}
62
+ else:
63
+ local_ns = "/".join(local_parts[:-1])
64
+ platform_data.setdefault(local_ns, {})[local_name] = None
65
+
66
+
67
+ def cmd_repos(config, base, config_path):
68
+ """Diff .pgit config against the filesystem and offer to update."""
69
+
70
+ # ── 1. Scan filesystem ──────────────────────────────────────────────────
71
+ local_repos = {} # "platform/local_path" -> (dir, remote_url)
72
+ for platform in ["github", "gitlab"]:
73
+ platform_dir = base / platform
74
+ if not platform_dir.exists():
75
+ continue
76
+ for git_marker in sorted(platform_dir.rglob(".git")):
77
+ if not git_marker.is_dir():
78
+ continue
79
+ repo_dir = git_marker.parent
80
+ rel = repo_dir.relative_to(platform_dir)
81
+ parts = rel.parts
82
+ # Skip repos nested inside another git repo
83
+ if any((platform_dir.joinpath(*parts[:i]) / ".git").is_dir()
84
+ for i in range(1, len(parts))):
85
+ continue
86
+ result = subprocess.run(
87
+ ["git", "-C", str(repo_dir), "remote", "get-url", "origin"],
88
+ capture_output=True, text=True
89
+ )
90
+ remote_url = result.stdout.strip() if result.returncode == 0 else None
91
+ local_repos[f"{platform}/{rel}"] = (repo_dir, remote_url)
92
+
93
+ # ── 2. Build tracked indices ─────────────────────────────────────────────
94
+ tracked_by_local = {} # "platform/local_path" -> (remote_path, expected_url)
95
+ tracked_by_remote = {} # "platform/remote_path" -> "platform/local_path"
96
+
97
+ for platform, namespaces in config.items():
98
+ host = HOSTS.get(platform, "")
99
+ for ns_key, ns_value in namespaces.items():
100
+ for local_path, remote_path in walk(ns_value, ns_key):
101
+ lkey = f"{platform}/{local_path}"
102
+ rkey = f"{platform}/{remote_path}"
103
+ tracked_by_local[lkey] = (remote_path, f"{host}:{remote_path}.git")
104
+ tracked_by_remote[rkey] = lkey
105
+
106
+ # ── 3. Categorize ────────────────────────────────────────────────────────
107
+ untracked = {} # key -> (dir, url)
108
+ wrong_location = {} # key -> (dir, url, config_key)
109
+ remote_mismatch = {} # key -> (expected_url, actual_url)
110
+ missing = {k for k in tracked_by_local if k not in local_repos}
111
+
112
+ for key, (repo_dir, remote_url) in local_repos.items():
113
+ fs_platform = key.split("/")[0]
114
+ _, remote_path = parse_remote_url(remote_url)
115
+ rkey = f"{fs_platform}/{remote_path}" if remote_path else None
116
+
117
+ if key in tracked_by_local:
118
+ _, expected_url = tracked_by_local[key]
119
+ if remote_url and remote_url != expected_url:
120
+ remote_mismatch[key] = (expected_url, remote_url)
121
+ elif rkey and rkey in tracked_by_remote:
122
+ wrong_location[key] = (repo_dir, remote_url, tracked_by_remote[rkey])
123
+ else:
124
+ untracked[key] = (repo_dir, remote_url)
125
+
126
+ # ── 4. Report ─────────────────────────────────────────────────────────────
127
+ if not any([untracked, wrong_location, remote_mismatch, missing]):
128
+ print(".pgit is in sync with the filesystem.")
129
+ return
130
+
131
+ if remote_mismatch:
132
+ print("\nRemote mismatch (local path matches config but git remote differs):")
133
+ for key, (expected, actual) in sorted(remote_mismatch.items()):
134
+ print(f" ~ {key}")
135
+ print(f" config: {expected}")
136
+ print(f" actual: {actual}")
137
+
138
+ if wrong_location:
139
+ print("\nCloned to a different path than .pgit expects:")
140
+ for key, (_, url, config_key) in sorted(wrong_location.items()):
141
+ actual_dir = base / Path(*key.split("/"))
142
+ expected_dir = base / Path(*config_key.split("/"))
143
+ is_rename = actual_dir.parent == expected_dir.parent
144
+ hint = f" (rename: {actual_dir.name} → {expected_dir.name})" if is_rename else ""
145
+ print(f" ? {key}")
146
+ print(f" remote: {url}")
147
+ print(f" expected: {config_key}{hint}")
148
+
149
+ if untracked:
150
+ print("\nOn disk but not in .pgit:")
151
+ for key, (_, url) in sorted(untracked.items()):
152
+ print(f" + {key} ({url or 'no remote'})")
153
+
154
+ if missing:
155
+ print("\nIn .pgit but not cloned locally (run: pgit sync):")
156
+ for key in sorted(missing):
157
+ print(f" - {key}")
158
+
159
+ # ── 5. Add untracked ──────────────────────────────────────────────────────
160
+ if untracked:
161
+ print()
162
+ if input("Add untracked repos to .pgit? [y/N] ").strip().lower() == "y":
163
+ for key, (_, remote_url) in sorted(untracked.items()):
164
+ fs_platform, local_path = key.split("/", 1)
165
+ _add_to_config(config, fs_platform, local_path, remote_url)
166
+ print(f" Added {key}")
167
+ write_config(config, config_path)
168
+ print(f"\nUpdated {config_path}")
169
+
170
+ # ── 6. Fix wrong-location ─────────────────────────────────────────────────
171
+ if wrong_location:
172
+ for key, (_, remote_url, config_key) in sorted(wrong_location.items()):
173
+ actual_dir = base / Path(*key.split("/"))
174
+ expected_dir = base / Path(*config_key.split("/"))
175
+ is_rename = actual_dir.parent == expected_dir.parent
176
+ print()
177
+ if is_rename:
178
+ if input(f"Rename {actual_dir.name} → {expected_dir.name}? [y/N] ").strip().lower() == "y":
179
+ actual_dir.rename(expected_dir)
180
+ print(f" Renamed.")
181
+ else:
182
+ if input(f"Add {key} to .pgit with remote override? [y/N] ").strip().lower() == "y":
183
+ fs_platform, local_path = key.split("/", 1)
184
+ _add_to_config(config, fs_platform, local_path, remote_url)
185
+ print(f" Added {key}")
186
+ write_config(config, config_path)
187
+ print(f"\nUpdated {config_path}")
188
+
189
+ # ── 7. Remove missing ─────────────────────────────────────────────────────
190
+ if missing:
191
+ print()
192
+ if input("Remove missing repos from .pgit? [y/N] ").strip().lower() == "y":
193
+ for key in sorted(missing):
194
+ fs_platform, local_path = key.split("/", 1)
195
+ for ns_key in list(config.get(fs_platform, {}).keys()):
196
+ if local_path.startswith(ns_key + "/"):
197
+ sub = local_path[len(ns_key)+1:].split("/")
198
+ del_nested(config[fs_platform][ns_key], sub)
199
+ if not config[fs_platform][ns_key]:
200
+ del config[fs_platform][ns_key]
201
+ break
202
+ print(f" Removed {key}")
203
+ write_config(config, config_path)
204
+ print(f"\nUpdated {config_path}")
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: polygit
3
+ Version: 0.1.0
4
+ Summary: Manage multiple git repos from a single .pgit config file.
5
+ Project-URL: Repository, https://gitlab.com/gary.schaetz/public/polygit
6
+ Author-email: Gary Schaetz <gary@schaetzkc.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 Gary Schaetz
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE.txt
29
+ Keywords: devtools,git,multi-repo,pgit,polygit,repos
30
+ Requires-Python: >=3.8
31
+ Requires-Dist: pyyaml>=6.0
32
+ Description-Content-Type: text/markdown
33
+
34
+ # polygit
35
+
36
+ Manage multiple git repos from a single `.pgit` config file.
37
+
38
+ ```bash
39
+ pip install polygit
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ pgit sync # clone missing repos, fetch all
46
+ pgit status # show repos with uncommitted changes
47
+ pgit pull # pull all repos, auto-stashing local changes
48
+ pgit push # interactively commit and push dirty repos
49
+ pgit repos # discover drift between .pgit and the filesystem
50
+ ```
51
+
52
+ ## Configuration
53
+
54
+ Create a `.pgit` file in the root directory that contains your repos:
55
+
56
+ ```yaml
57
+ github:
58
+ your-username:
59
+ repo-one:
60
+ repo-two:
61
+ some-org:
62
+ their-repo: {remote: "some-org/their-repo"}
63
+
64
+ gitlab:
65
+ your-namespace/group:
66
+ private-repo:
67
+ another-repo:
68
+ ```
69
+
70
+ `pgit` discovers this file by walking up from the current directory — so running `pgit status` anywhere inside your repos root just works.
71
+
72
+ ### Remote overrides
73
+
74
+ If a repo is cloned to a local path that differs from its remote path, use the `remote` key:
75
+
76
+ ```yaml
77
+ github:
78
+ my-local-name:
79
+ project: {remote: "upstream-org/project"}
80
+ ```
81
+
82
+ ## push modes
83
+
84
+ ```bash
85
+ pgit push # prompt for a message per repo (default)
86
+ pgit push --all # one message for all dirty repos
87
+ pgit push --wip # auto-commit with "WIP <timestamp>"
88
+ ```
89
+
90
+ ## repos — drift detection
91
+
92
+ `pgit repos` compares what's actually on disk against your `.pgit` config and reports:
93
+
94
+ - **Remote mismatch** — repo is at the right path but the git remote differs (e.g. SSH vs HTTPS)
95
+ - **Wrong location** — remote is tracked in `.pgit` but cloned to a different directory
96
+ - **Untracked** — repo on disk not referenced in `.pgit` at all
97
+ - **Missing** — in `.pgit` but not yet cloned (run `pgit sync`)
98
+
99
+ It then offers to update `.pgit` or rename directories interactively.
100
+
101
+ ## Migrating from repos.yaml
102
+
103
+ If you were using the `sync.sh` predecessor, rename your `repos.yaml` to `.pgit` — the format is identical.
104
+
105
+ ## Requirements
106
+
107
+ - Python 3.8+
108
+ - git
@@ -0,0 +1,10 @@
1
+ polygit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ polygit/cli.py,sha256=t55G_27aWlqScA3mQeRVFg1HwitpzBYOATn3qVl7Ulk,2163
3
+ polygit/config.py,sha256=3lZCp073173cskTxXpqNjP8bk50QuYstz0zKLCEieQY,1378
4
+ polygit/repo.py,sha256=92l_OO63KBIdho-Sq1KaWoRhbLT616trA08Ct03GRlE,5384
5
+ polygit/repos.py,sha256=1X4xpA7d32cdHt--v2-1yRjw4uiMxGRrWz-uMw27zfE,9602
6
+ polygit-0.1.0.dist-info/METADATA,sha256=5vNz9Ny6u74O6Zk8kWJp1Wf5bPZy0hKrY3r8MS9Wjb8,3593
7
+ polygit-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ polygit-0.1.0.dist-info/entry_points.txt,sha256=hBPbWbQygzHXMfwect-YNoaVPFh1_9EMfIo4ec3EY_A,42
9
+ polygit-0.1.0.dist-info/licenses/LICENSE.txt,sha256=YxFgIp_vc7jYYhGaqNWuAcBNNenp4Kgnj622VLgew_U,1069
10
+ polygit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pgit = polygit.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gary Schaetz
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.