pulli 0.2.1__tar.gz → 0.2.6__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.
@@ -16,5 +16,11 @@ env/
16
16
  dist/
17
17
  build/
18
18
 
19
+ # Local secrets / env
20
+ .env
21
+ .env.local
22
+ .env.*
23
+ !.env.example
24
+
19
25
  # OS
20
26
  .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pulli
3
- Version: 0.2.1
3
+ Version: 0.2.6
4
4
  Summary: Walk a directory tree, show git repos with status, and fast-forward the ones that are behind upstream.
5
5
  Author: Stefan Waldherr
6
6
  License-Expression: MIT
@@ -22,16 +22,34 @@ fast-forward the ones that are behind upstream.
22
22
 
23
23
  ## Install
24
24
 
25
+ ### Permanently (on your PATH)
26
+
25
27
  ```bash
26
- uv tool install git+https://github.com/devskale/pulli
28
+ uv tool install pulli
27
29
  # or
28
- pipx install git+https://github.com/devskale/pulli
30
+ pipx install pulli
29
31
  ```
30
32
 
31
- Both put `pulli` on your PATH (`~/.local/bin/pulli`). To install a local
32
- checkout instead (development):
33
+ Both put `pulli` on your PATH (`~/.local/bin/pulli`). After that just run
34
+ `pulli`.
35
+
36
+ ### Once, without installing (`uvx`)
33
37
 
34
38
  ```bash
39
+ uvx pulli
40
+ uvx pulli ~/code
41
+ ```
42
+
43
+ `uvx` fetches `pulli` from PyPI into a temporary cache and runs it
44
+ immediately — no permanent install, no PATH entry. Great for a one-off run
45
+ or to try it out. `uv run --from pulli pulli` does the same thing inside a
46
+ `uv run` context.
47
+
48
+ ### From Git / a dev checkout
49
+
50
+ ```bash
51
+ uv tool install git+https://github.com/devskale/pulli
52
+ # or, for a local checkout (development)
35
53
  cd ~/code/pulli
36
54
  uv tool install --force .
37
55
  ```
@@ -57,12 +75,20 @@ connectors).
57
75
  | flag | effect |
58
76
  | --- | --- |
59
77
  | `--tree` | show the full directory tree instead of the flat repo list |
78
+ | `--behind` | only the repos that need a pull |
79
+ | `--attention` | only dirty / diverged / behind / broken repos — what needs you first |
80
+ | `--no-summary` | omit the one-line summary under the list |
60
81
  | `--no-fetch` | don't fetch remotes before showing status |
61
82
  | `--no-symlinks` | don't follow symlinks (default: follow, marked) |
62
83
  | `--max-depth N` | limit recursion (default: 50) |
63
- | `--no-color` | disable ANSI colours (also implied off a TTY) |
84
+ | `--no-color` | disable ANSI colours (also implied off a TTY, by `NO_COLOR`, or `TERM=dumb`) |
64
85
  | `--json` | one JSON object per line, for scripts |
65
86
 
87
+ `pulli --examples` prints a cheat sheet of common invocations.
88
+
89
+ The list ends with a one-line summary (`56 repos · 16 behind · 1 diverged …`),
90
+ so a scan answers "how are my repos doing?" without counting lines.
91
+
66
92
  Fetches run in parallel by default, so ahead/behind is current, and each is
67
93
  bounded by a short timeout — if you're offline (on a train) unreachable
68
94
  remotes are reported as `offline` and the tree still renders.
@@ -73,6 +99,20 @@ frozen. The spinner only appears on a real terminal; when output is piped
73
99
  or captured (`pulli | less`, CI, scripts) it is a no-op and the output stays
74
100
  byte-clean. `--no-color` disables the spinner's colouring too.
75
101
 
102
+ ### `update`
103
+
104
+ Checks PyPI and upgrades pulli itself — the installer that owns the binary
105
+ (`uv tool` or `pipx`) decides the upgrade command.
106
+
107
+ ```bash
108
+ pulli update # upgrade to the latest PyPI release
109
+ pulli update --check # show what would be updated, change nothing
110
+ ```
111
+
112
+ `pulli --version` also reports when a newer release exists. The check hits
113
+ PyPI at most once a day (cached in the platform cache dir), never touches
114
+ the output of normal runs, and is silent when offline.
115
+
76
116
  ### `pull`
77
117
 
78
118
  | flag | effect |
@@ -92,9 +132,10 @@ corrupt work to make progress.
92
132
  | repo state | action |
93
133
  | --- | --- |
94
134
  | behind upstream, clean | `git pull --ff-only` |
135
+ | behind upstream, **untracked files only** | `git pull --ff-only` — untracked files cannot conflict with a fast-forward, so they are kept and the repo is pulled (marked `untracked N` instead of `dirty N`) |
95
136
  | up to date | nothing (silent) |
96
137
  | ahead only | nothing — local commits need a *push*, not a pull |
97
- | uncommitted changes | skipped, reported |
138
+ | uncommitted changes (tracked) | skipped, with the way out: *commit or stash, then pulli pull again* |
98
139
  | diverged (ahead **and** behind) | skipped — a fast-forward is impossible; merge or rebase is your call |
99
140
  | merge / rebase / bisect in progress | skipped, reported — never touched |
100
141
  | no upstream (detached HEAD, no remote) | nothing to pull |
@@ -126,7 +167,7 @@ Would pull 1 repo(s).
126
167
  ### Flat list (default)
127
168
 
128
169
  ```
129
- clones/pi earendil-works/pi main ↓0 ↑0 ● clean
170
+ clones/pi earendil-works/pi main ↓0 ↑0 ●
130
171
  clones/gogcli openclaw/gogcli main ↓0 ↑0 ◐ dirty 5
131
172
  kontext.one devskale/kontext.one main ↓0 ↑0 ◐ dirty 5
132
173
  ```
@@ -139,13 +180,13 @@ sorted by path, so a run is reproducible.
139
180
  ```
140
181
  ~/code
141
182
  ├── clones/
142
- │ ├── herdr ogulcancelik/herdr master ↓0 ↑0 ● clean
143
- │ ├── pi earendil-works/pi main ↓33 ↑0 ● clean
183
+ │ ├── herdr ogulcancelik/herdr master ↓0 ↑0 ●
184
+ │ ├── pi earendil-works/pi main ↓33 ↑0 ●
144
185
  │ └── gogcli openclaw/gogcli main ↓0 ↑0 ◐ dirty 5
145
186
  ├── handoffs -> code/skaleshare/handoffs (alias)
146
187
  ├── kontext.one devskale/kontext.one main ↓0 ↑0 ◐ dirty 5
147
- │ ├── klark0 devskale/klark0 dev ↓0 ↑0 ● clean
148
- │ └── python-utils (submodule) ↓0 ↑0 ● clean
188
+ │ ├── klark0 devskale/klark0 dev ↓0 ↑0 ●
189
+ │ └── python-utils (submodule) ↓0 ↑0 ●
149
190
  └── backups/
150
191
  └── model-proxy.git/ (bare repo — nothing to pull)
151
192
  ```
@@ -155,13 +196,13 @@ sorted by path, so a run is reproducible.
155
196
  ```
156
197
  ~/code
157
198
  ├── clones/
158
- │ ├── herdr ogulcancelik/herdr master ↓0 ↑0 ● clean
159
- │ ├── pi earendil-works/pi main ↓33 ↑0 ● clean
199
+ │ ├── herdr ogulcancelik/herdr master ↓0 ↑0 ●
200
+ │ ├── pi earendil-works/pi main ↓33 ↑0 ●
160
201
  │ └── gogcli openclaw/gogcli main ↓0 ↑0 ◐ dirty 5
161
202
  ├── handoffs -> code/skaleshare/handoffs (alias)
162
203
  ├── kontext.one devskale/kontext.one main ↓0 ↑0 ◐ dirty 5
163
- │ ├── klark0 devskale/klark0 dev ↓0 ↑0 ● clean
164
- │ └── python-utils (submodule) ↓0 ↑0 ● clean
204
+ │ ├── klark0 devskale/klark0 dev ↓0 ↑0 ●
205
+ │ └── python-utils (submodule) ↓0 ↑0 ●
165
206
  └── backups/
166
207
  └── model-proxy.git/ (bare repo — nothing to pull)
167
208
  ```
@@ -169,8 +210,10 @@ sorted by path, so a run is reproducible.
169
210
  - **`↓N ↑M`** — commits behind / ahead of upstream; `· ·` means there is
170
211
  no upstream to compare against (a fresh `git init`, a detached HEAD, a
171
212
  remote-less clone), which is not the same as "in sync"
172
- - **`●` clean / `◐` dirty N / `✗` error** — `◐ offline` means the fetch
173
- failed, so the numbers may be stale
213
+ - **`●` clean (default) / `◐` dirty N / `✗` error** — `◐ offline` means the fetch
214
+ failed, so the numbers may be stale. Untracked-only content renders as
215
+ `◐ untracked N` (dim), not dirty: git itself does not consider untracked
216
+ content dirty, and a pull cannot conflict with it
174
217
  - **symlinks** are followed by default and marked; one whose target is also
175
218
  reachable under its real name is shown as an `(alias)` and is never a
176
219
  second pull target. `--no-symlinks` skips them
@@ -5,16 +5,34 @@ fast-forward the ones that are behind upstream.
5
5
 
6
6
  ## Install
7
7
 
8
+ ### Permanently (on your PATH)
9
+
8
10
  ```bash
9
- uv tool install git+https://github.com/devskale/pulli
11
+ uv tool install pulli
10
12
  # or
11
- pipx install git+https://github.com/devskale/pulli
13
+ pipx install pulli
12
14
  ```
13
15
 
14
- Both put `pulli` on your PATH (`~/.local/bin/pulli`). To install a local
15
- checkout instead (development):
16
+ Both put `pulli` on your PATH (`~/.local/bin/pulli`). After that just run
17
+ `pulli`.
18
+
19
+ ### Once, without installing (`uvx`)
16
20
 
17
21
  ```bash
22
+ uvx pulli
23
+ uvx pulli ~/code
24
+ ```
25
+
26
+ `uvx` fetches `pulli` from PyPI into a temporary cache and runs it
27
+ immediately — no permanent install, no PATH entry. Great for a one-off run
28
+ or to try it out. `uv run --from pulli pulli` does the same thing inside a
29
+ `uv run` context.
30
+
31
+ ### From Git / a dev checkout
32
+
33
+ ```bash
34
+ uv tool install git+https://github.com/devskale/pulli
35
+ # or, for a local checkout (development)
18
36
  cd ~/code/pulli
19
37
  uv tool install --force .
20
38
  ```
@@ -40,12 +58,20 @@ connectors).
40
58
  | flag | effect |
41
59
  | --- | --- |
42
60
  | `--tree` | show the full directory tree instead of the flat repo list |
61
+ | `--behind` | only the repos that need a pull |
62
+ | `--attention` | only dirty / diverged / behind / broken repos — what needs you first |
63
+ | `--no-summary` | omit the one-line summary under the list |
43
64
  | `--no-fetch` | don't fetch remotes before showing status |
44
65
  | `--no-symlinks` | don't follow symlinks (default: follow, marked) |
45
66
  | `--max-depth N` | limit recursion (default: 50) |
46
- | `--no-color` | disable ANSI colours (also implied off a TTY) |
67
+ | `--no-color` | disable ANSI colours (also implied off a TTY, by `NO_COLOR`, or `TERM=dumb`) |
47
68
  | `--json` | one JSON object per line, for scripts |
48
69
 
70
+ `pulli --examples` prints a cheat sheet of common invocations.
71
+
72
+ The list ends with a one-line summary (`56 repos · 16 behind · 1 diverged …`),
73
+ so a scan answers "how are my repos doing?" without counting lines.
74
+
49
75
  Fetches run in parallel by default, so ahead/behind is current, and each is
50
76
  bounded by a short timeout — if you're offline (on a train) unreachable
51
77
  remotes are reported as `offline` and the tree still renders.
@@ -56,6 +82,20 @@ frozen. The spinner only appears on a real terminal; when output is piped
56
82
  or captured (`pulli | less`, CI, scripts) it is a no-op and the output stays
57
83
  byte-clean. `--no-color` disables the spinner's colouring too.
58
84
 
85
+ ### `update`
86
+
87
+ Checks PyPI and upgrades pulli itself — the installer that owns the binary
88
+ (`uv tool` or `pipx`) decides the upgrade command.
89
+
90
+ ```bash
91
+ pulli update # upgrade to the latest PyPI release
92
+ pulli update --check # show what would be updated, change nothing
93
+ ```
94
+
95
+ `pulli --version` also reports when a newer release exists. The check hits
96
+ PyPI at most once a day (cached in the platform cache dir), never touches
97
+ the output of normal runs, and is silent when offline.
98
+
59
99
  ### `pull`
60
100
 
61
101
  | flag | effect |
@@ -75,9 +115,10 @@ corrupt work to make progress.
75
115
  | repo state | action |
76
116
  | --- | --- |
77
117
  | behind upstream, clean | `git pull --ff-only` |
118
+ | behind upstream, **untracked files only** | `git pull --ff-only` — untracked files cannot conflict with a fast-forward, so they are kept and the repo is pulled (marked `untracked N` instead of `dirty N`) |
78
119
  | up to date | nothing (silent) |
79
120
  | ahead only | nothing — local commits need a *push*, not a pull |
80
- | uncommitted changes | skipped, reported |
121
+ | uncommitted changes (tracked) | skipped, with the way out: *commit or stash, then pulli pull again* |
81
122
  | diverged (ahead **and** behind) | skipped — a fast-forward is impossible; merge or rebase is your call |
82
123
  | merge / rebase / bisect in progress | skipped, reported — never touched |
83
124
  | no upstream (detached HEAD, no remote) | nothing to pull |
@@ -109,7 +150,7 @@ Would pull 1 repo(s).
109
150
  ### Flat list (default)
110
151
 
111
152
  ```
112
- clones/pi earendil-works/pi main ↓0 ↑0 ● clean
153
+ clones/pi earendil-works/pi main ↓0 ↑0 ●
113
154
  clones/gogcli openclaw/gogcli main ↓0 ↑0 ◐ dirty 5
114
155
  kontext.one devskale/kontext.one main ↓0 ↑0 ◐ dirty 5
115
156
  ```
@@ -122,13 +163,13 @@ sorted by path, so a run is reproducible.
122
163
  ```
123
164
  ~/code
124
165
  ├── clones/
125
- │ ├── herdr ogulcancelik/herdr master ↓0 ↑0 ● clean
126
- │ ├── pi earendil-works/pi main ↓33 ↑0 ● clean
166
+ │ ├── herdr ogulcancelik/herdr master ↓0 ↑0 ●
167
+ │ ├── pi earendil-works/pi main ↓33 ↑0 ●
127
168
  │ └── gogcli openclaw/gogcli main ↓0 ↑0 ◐ dirty 5
128
169
  ├── handoffs -> code/skaleshare/handoffs (alias)
129
170
  ├── kontext.one devskale/kontext.one main ↓0 ↑0 ◐ dirty 5
130
- │ ├── klark0 devskale/klark0 dev ↓0 ↑0 ● clean
131
- │ └── python-utils (submodule) ↓0 ↑0 ● clean
171
+ │ ├── klark0 devskale/klark0 dev ↓0 ↑0 ●
172
+ │ └── python-utils (submodule) ↓0 ↑0 ●
132
173
  └── backups/
133
174
  └── model-proxy.git/ (bare repo — nothing to pull)
134
175
  ```
@@ -138,13 +179,13 @@ sorted by path, so a run is reproducible.
138
179
  ```
139
180
  ~/code
140
181
  ├── clones/
141
- │ ├── herdr ogulcancelik/herdr master ↓0 ↑0 ● clean
142
- │ ├── pi earendil-works/pi main ↓33 ↑0 ● clean
182
+ │ ├── herdr ogulcancelik/herdr master ↓0 ↑0 ●
183
+ │ ├── pi earendil-works/pi main ↓33 ↑0 ●
143
184
  │ └── gogcli openclaw/gogcli main ↓0 ↑0 ◐ dirty 5
144
185
  ├── handoffs -> code/skaleshare/handoffs (alias)
145
186
  ├── kontext.one devskale/kontext.one main ↓0 ↑0 ◐ dirty 5
146
- │ ├── klark0 devskale/klark0 dev ↓0 ↑0 ● clean
147
- │ └── python-utils (submodule) ↓0 ↑0 ● clean
187
+ │ ├── klark0 devskale/klark0 dev ↓0 ↑0 ●
188
+ │ └── python-utils (submodule) ↓0 ↑0 ●
148
189
  └── backups/
149
190
  └── model-proxy.git/ (bare repo — nothing to pull)
150
191
  ```
@@ -152,8 +193,10 @@ sorted by path, so a run is reproducible.
152
193
  - **`↓N ↑M`** — commits behind / ahead of upstream; `· ·` means there is
153
194
  no upstream to compare against (a fresh `git init`, a detached HEAD, a
154
195
  remote-less clone), which is not the same as "in sync"
155
- - **`●` clean / `◐` dirty N / `✗` error** — `◐ offline` means the fetch
156
- failed, so the numbers may be stale
196
+ - **`●` clean (default) / `◐` dirty N / `✗` error** — `◐ offline` means the fetch
197
+ failed, so the numbers may be stale. Untracked-only content renders as
198
+ `◐ untracked N` (dim), not dirty: git itself does not consider untracked
199
+ content dirty, and a pull cannot conflict with it
157
200
  - **symlinks** are followed by default and marked; one whose target is also
158
201
  reachable under its real name is shown as an `(alias)` and is never a
159
202
  second pull target. `--no-symlinks` skips them
@@ -0,0 +1,85 @@
1
+ #!/bin/bash
2
+ # Local release builder — zero GitHub Actions minutes.
3
+ # Gates: clean worktree → tests green → version bump → tag → build →
4
+ # (optional) publish to PyPI.
5
+ #
6
+ # Usage:
7
+ # bash scripts/release.sh 0.2.2 # bump, tag, build
8
+ # bash scripts/release.sh 0.2.2 --publish # ... and upload to PyPI
9
+ #
10
+ # Publish needs a PyPI token: UV_PUBLISH_TOKEN in the env, or pass it
11
+ # when prompted. Everything else runs fully offline.
12
+ set -euo pipefail
13
+
14
+ REPO="devskale/pulli"
15
+ VERSION="${1:?usage: release.sh <version> [--publish] (no leading v)}"
16
+ VERSION="${VERSION#v}"
17
+ TAG="v$VERSION"
18
+ PUBLISH="${2:-}"
19
+
20
+ cd "$(dirname "$0")/.."
21
+
22
+ echo "── Gate: clean worktree ──"
23
+ if [ -n "$(git status --porcelain)" ]; then
24
+ echo " worktree is dirty — commit or stash first:" >&2
25
+ git status --porcelain >&2
26
+ exit 1
27
+ fi
28
+
29
+ echo "── Gate: on main, up to date with origin ──"
30
+ BRANCH="$(git rev-parse --abbrev-ref HEAD)"
31
+ [ "$BRANCH" = "main" ] || { echo " not on main (is $BRANCH)" >&2; exit 1; }
32
+ git fetch -q origin main
33
+ [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] || {
34
+ echo " main is not in sync with origin/main — push/pull first" >&2
35
+ exit 1
36
+ }
37
+
38
+ echo "── Gate: tag does not exist yet ──"
39
+ git rev-parse -q --verify "refs/tags/$TAG" >/dev/null && {
40
+ echo " $TAG already exists" >&2
41
+ exit 1
42
+ }
43
+
44
+ echo "── Gate: tests green ──"
45
+ uv run --extra dev pytest
46
+
47
+ echo "── Bump version → $VERSION ──"
48
+ INIT="src/pulli/__init__.py"
49
+ sed -i '' "s/^__version__ = .*/__version__ = \"$VERSION\"/" "$INIT"
50
+ git add "$INIT" uv.lock
51
+ git commit -q -m "v$VERSION" -m "Released via scripts/release.sh (local build, zero CI minutes)."
52
+ git tag -a "$TAG" -m "v$VERSION"
53
+
54
+ echo "── Sanity: built wheel reports the right version ──"
55
+ rm -rf dist && mkdir -p dist
56
+ uv build -q
57
+ uv run --no-project --with "./dist/pulli-$VERSION-py3-none-any.whl" \
58
+ pulli --version | grep -Fx "pulli $VERSION" || {
59
+ echo " version mismatch in wheel" >&2
60
+ exit 1
61
+ }
62
+
63
+ echo "── Push commit + tag ──"
64
+ git push origin main "refs/tags/$TAG"
65
+
66
+ if [ "$PUBLISH" = "--publish" ]; then
67
+ echo "── Publish to PyPI ──"
68
+ # Token sources, in order: env → credgoo (Airtable backend).
69
+ # credgoo is the canonical store; .env files are not (no second copy).
70
+ if [ -z "${UV_PUBLISH_TOKEN:-}" ] && command -v credgoo >/dev/null 2>&1; then
71
+ UV_PUBLISH_TOKEN="$(credgoo UV_PUBLISH_TOKEN 2>/dev/null)" || true
72
+ export UV_PUBLISH_TOKEN
73
+ fi
74
+ if [ -z "${UV_PUBLISH_TOKEN:-}" ]; then
75
+ echo " UV_PUBLISH_TOKEN not set — get one at pypi.org (account settings"
76
+ echo " → API tokens, scope: this project) and re-run with it set:"
77
+ echo " UV_PUBLISH_TOKEN=pypi-... bash scripts/release.sh $VERSION --publish"
78
+ exit 1
79
+ fi
80
+ uv publish "dist/pulli-$VERSION-py3-none-any.whl" "dist/pulli-$VERSION.tar.gz"
81
+ echo "── Done: https://pypi.org/project/pulli/$VERSION ──"
82
+ else
83
+ echo "── Built (not published). To publish: ──"
84
+ echo " UV_PUBLISH_TOKEN=pypi-... uv publish dist/pulli-$VERSION*"
85
+ fi
@@ -1,2 +1,2 @@
1
1
  """pulli — discover git repos under a root and report their status."""
2
- __version__ = "0.2.1"
2
+ __version__ = "0.2.6"