polygit 0.3.1__tar.gz → 0.3.2__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.
@@ -1,3 +1,8 @@
1
+ include:
2
+ - project: "gary.schaetz/private/pipelines"
3
+ ref: main
4
+ file: 'templates/rules/rules.yml'
5
+
1
6
  stages:
2
7
  - test
3
8
  - build
@@ -12,6 +17,14 @@ test:
12
17
  tags:
13
18
  - docker
14
19
  - kubernetes
20
+ # Explicit rules instead of relying on GitLab's implicit default (only: [branches, tags]):
21
+ # that default happens to exclude merge_request_event already, but making it explicit here
22
+ # matches the pattern every other repo running on these self-hosted runners uses, rather
23
+ # than depending on undocumented default behavior for a job that runs arbitrary repo code.
24
+ rules:
25
+ - !reference [.job_rules, standard-rule]
26
+ - !reference [.job_rules, manual-rule]
27
+ - !reference [.job_rules, schedule-rule]
15
28
  script:
16
29
  - uv sync
17
30
  - uv run pytest
@@ -0,0 +1,15 @@
1
+ # Repo context for AI agents
2
+
3
+ Published Python package (`pip install polygit`, installed via `uv tool install` per the
4
+ user's `setup-new-machine.sh` bootstrap docs — see project memory) providing the `pgit` CLI:
5
+ manages multiple git repos (across GitHub/GitLab, multiple orgs/namespaces) from a single
6
+ `.pgit` config file. `.pgit` is discovered by walking up from the current directory, so
7
+ `pgit` commands work from anywhere inside the repos root — this is in fact what manages the
8
+ very `~/development/gitlab` tree these AGENTS.md files live in.
9
+
10
+ Standard `src`-layout Python package (`pyproject.toml`, `uv.lock` — uses `uv`, not
11
+ pip/poetry, for dev dependency management). `.gitlab-ci.yml` presumably builds/publishes on
12
+ tag, mirroring `gitlab-ci-generator`'s pattern.
13
+
14
+ Replaced an older `sync.sh` script (see recent commit history) — if you find references to
15
+ `sync.sh` elsewhere in the user's dotfiles/bootstrap scripts, they're stale.
@@ -0,0 +1,3 @@
1
+ # Claude Code context
2
+
3
+ See [AGENTS.md](AGENTS.md) for repo overview.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polygit
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Manage multiple git repos from a single .pgit config file.
5
5
  Project-URL: Repository, https://gitlab.com/gary.schaetz/public/polygit
6
6
  Author-email: Gary Schaetz <gary@schaetzkc.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "polygit"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Manage multiple git repos from a single .pgit config file."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE.txt" }
@@ -1,6 +1,8 @@
1
+ import shutil
1
2
  import subprocess
2
3
  import sys
3
4
  import threading
5
+ import time
4
6
  from concurrent.futures import ThreadPoolExecutor, as_completed
5
7
  from datetime import datetime
6
8
  from pathlib import Path
@@ -9,6 +11,39 @@ from .config import HOSTS
9
11
  _print_lock = threading.Lock()
10
12
  MAX_WORKERS = 8
11
13
 
14
+ # Errors seen when the SSH server drops a connection under concurrent load
15
+ # rather than a real failure — safe to retry.
16
+ _TRANSIENT_ERR_MARKERS = (
17
+ "kex_exchange_identification",
18
+ "Connection reset by peer",
19
+ "Connection timed out",
20
+ "Could not read from remote repository",
21
+ "early EOF",
22
+ "The remote end hung up unexpectedly",
23
+ )
24
+
25
+
26
+ def _is_transient(stderr):
27
+ return any(marker in stderr for marker in _TRANSIENT_ERR_MARKERS)
28
+
29
+
30
+ def _run_git(args, retries=2, delay=1.5, cleanup=None):
31
+ """Run a git subprocess, retrying on transient SSH/network errors.
32
+
33
+ `cleanup`, if given, runs before each retry (e.g. to remove a partial
34
+ clone directory left behind by the failed attempt).
35
+ """
36
+ for attempt in range(retries + 1):
37
+ result = subprocess.run(args, capture_output=True, text=True)
38
+ if result.returncode == 0 or not _is_transient(result.stderr):
39
+ return result
40
+ if attempt < retries:
41
+ if cleanup:
42
+ cleanup()
43
+ time.sleep(delay * (attempt + 1))
44
+ return result
45
+
46
+
12
47
  _USE_COLOR = sys.stdout.isatty()
13
48
 
14
49
  _G = "\033[32m" if _USE_COLOR else "" # green
@@ -72,7 +107,7 @@ def _commit_and_push(d, label, msg):
72
107
  lines = [f" Committing {label}..."]
73
108
  subprocess.run(["git", "-C", str(d), "add", "-A"], capture_output=True)
74
109
  subprocess.run(["git", "-C", str(d), "commit", "-m", msg], capture_output=True)
75
- result = subprocess.run(["git", "-C", str(d), "push"], capture_output=True, text=True)
110
+ result = _run_git(["git", "-C", str(d), "push"])
76
111
  if result.returncode != 0:
77
112
  lines.append(f"{ERR} {label}: push failed — {result.stderr.strip().splitlines()[0]}")
78
113
  else:
@@ -84,19 +119,16 @@ def _sync_one(platform, local_path, remote_path, d):
84
119
  url = f"{HOSTS[platform]}:{remote_path}.git"
85
120
  label = f"{platform}/{local_path}"
86
121
  if (d / ".git").exists():
87
- result = subprocess.run(
88
- ["git", "-C", str(d), "fetch", "--all", "-q"],
89
- capture_output=True, text=True
90
- )
122
+ result = _run_git(["git", "-C", str(d), "fetch", "--all", "-q"])
91
123
  if result.returncode != 0:
92
124
  lines = [f"{ERR} {label}: {result.stderr.strip().splitlines()[0]}"]
93
125
  else:
94
126
  lines = [f"{OK} {_DIM}{label}{_RST}"]
95
127
  else:
96
128
  d.parent.mkdir(parents=True, exist_ok=True)
97
- result = subprocess.run(
129
+ result = _run_git(
98
130
  ["git", "clone", url, str(d)],
99
- capture_output=True, text=True
131
+ cleanup=lambda: shutil.rmtree(d, ignore_errors=True),
100
132
  )
101
133
  if result.returncode != 0:
102
134
  lines = [f"{ERR} {label}: {result.stderr.strip().splitlines()[0]}"]
@@ -112,17 +144,14 @@ def _pull_one(platform, local_path, d):
112
144
  if is_dirty(d):
113
145
  subprocess.run(["git", "-C", str(d), "stash"], capture_output=True)
114
146
  stashed = True
115
- result = subprocess.run(
116
- ["git", "-C", str(d), "pull", "--rebase"],
117
- capture_output=True, text=True
118
- )
147
+ result = _run_git(["git", "-C", str(d), "pull", "--rebase"])
119
148
  if result.returncode != 0:
120
149
  err = result.stderr.strip()
121
150
  if "no tracking information" in err or "no upstream" in err.lower():
122
151
  lines.append(f"{WARN} {label}: no upstream branch set")
123
152
  else:
124
153
  lines.append(f"{ERR} {label}: {err.splitlines()[0]}")
125
- elif result.stdout.strip() and result.stdout.strip() != "Already up to date.":
154
+ elif result.stdout.strip() and "up to date" not in result.stdout.lower():
126
155
  lines.append(f"{UPD} {label}:")
127
156
  for ln in result.stdout.strip().splitlines():
128
157
  lines.append(f" {ln}")
@@ -90,7 +90,7 @@ wheels = [
90
90
 
91
91
  [[package]]
92
92
  name = "polygit"
93
- version = "0.1.0"
93
+ version = "0.3.2"
94
94
  source = { editable = "." }
95
95
  dependencies = [
96
96
  { name = "pyyaml" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes