pulli 0.2.11__tar.gz → 0.2.13__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.
- {pulli-0.2.11 → pulli-0.2.13}/PKG-INFO +1 -1
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/__init__.py +1 -1
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/cli.py +14 -8
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/discovery.py +81 -0
- {pulli-0.2.11 → pulli-0.2.13}/.gitignore +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/LICENSE +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/README.md +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/pyproject.toml +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/scripts/release.sh +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/pull.py +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/spinner.py +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/state.py +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/status.py +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/style.py +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/tree.py +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/src/pulli/update.py +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/tests/test_pulli.py +0 -0
- {pulli-0.2.11 → pulli-0.2.13}/uv.lock +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""pulli — discover git repos under a root and report their status."""
|
|
2
|
-
__version__ = "0.2.
|
|
2
|
+
__version__ = "0.2.13"
|
|
@@ -132,8 +132,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
132
132
|
tree_p.add_argument(
|
|
133
133
|
"--max-depth",
|
|
134
134
|
type=int,
|
|
135
|
-
default=
|
|
136
|
-
help="Maximum recursion depth (default:
|
|
135
|
+
default=2,
|
|
136
|
+
help="Maximum recursion depth (default: 2).",
|
|
137
137
|
)
|
|
138
138
|
tree_p.add_argument(
|
|
139
139
|
"--tree",
|
|
@@ -188,8 +188,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
188
188
|
pull_p.add_argument(
|
|
189
189
|
"--max-depth",
|
|
190
190
|
type=int,
|
|
191
|
-
default=
|
|
192
|
-
help="Maximum recursion depth (default:
|
|
191
|
+
default=2,
|
|
192
|
+
help="Maximum recursion depth (default: 2).",
|
|
193
193
|
)
|
|
194
194
|
|
|
195
195
|
# `pulli update` — self-update.
|
|
@@ -386,6 +386,12 @@ def _run_tree(args) -> int:
|
|
|
386
386
|
# Stream: print the skeleton immediately, then fill each repo's
|
|
387
387
|
# status line in as it becomes ready — no blank-screen wait.
|
|
388
388
|
live = LiveTree(tree, use_color=use_color, flat=flat)
|
|
389
|
+
if flat and not args.no_summary:
|
|
390
|
+
# The summary prints its own skeleton line right away ("N
|
|
391
|
+
# repos…") and rewrites it in place as statuses land — same
|
|
392
|
+
# deal as the repo lines, so the eye has the answer's shape from
|
|
393
|
+
# the first frame on.
|
|
394
|
+
print(f"{len(repos)} repos…")
|
|
389
395
|
fetch_and_status(
|
|
390
396
|
repos,
|
|
391
397
|
quiet=True,
|
|
@@ -394,10 +400,10 @@ def _run_tree(args) -> int:
|
|
|
394
400
|
on_done=live.update,
|
|
395
401
|
)
|
|
396
402
|
if flat and not args.no_summary:
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
|
|
400
|
-
|
|
403
|
+
# Rewrite the skeleton summary in place: one line up, clear,
|
|
404
|
+
# print the real one. The tree is done rewriting its lines, so
|
|
405
|
+
# the cursor is already at the bottom.
|
|
406
|
+
sys.stdout.write("\r\x1b[2K" + _summary(list(iter_repos(tree)), use_color) + "\n")
|
|
401
407
|
else:
|
|
402
408
|
# Not a terminal: no in-place cursor control. Do the work, then
|
|
403
409
|
# print the complete output once (byte-identical to before).
|
|
@@ -258,6 +258,13 @@ def _walk_links(
|
|
|
258
258
|
would win and the actual directory would disappear from the tree, which
|
|
259
259
|
is backwards. Walking links last means a real directory always wins, and
|
|
260
260
|
a link is only shown when nothing else reaches its target.
|
|
261
|
+
|
|
262
|
+
This pass also descends into plain directories the first walk skipped
|
|
263
|
+
(plain dirs inside a repo are never recorded as nodes) looking for
|
|
264
|
+
symlinks: a repo checked out *inside* another repo often keeps links to
|
|
265
|
+
sibling repos in a dot-dir (`.links/`), and those links are the whole
|
|
266
|
+
point of the layout. Plain dirs found this way are traversed, not
|
|
267
|
+
recorded — the tree stays as small as before.
|
|
261
268
|
"""
|
|
262
269
|
# As in _walk: a node deeper than max_depth is not expanded. The link
|
|
263
270
|
# pass must respect the same limit as the plain walk, or --max-depth
|
|
@@ -275,6 +282,23 @@ def _walk_links(
|
|
|
275
282
|
link = real_dir / name
|
|
276
283
|
try:
|
|
277
284
|
if not link.is_symlink():
|
|
285
|
+
# Not a link: still look *inside* for links (the .links/
|
|
286
|
+
# case) — but only one level of plain-dir descent, so a
|
|
287
|
+
# repo's src/ tree is not pointlessly scanned. The dir
|
|
288
|
+
# becomes a real node so links land under it, not under
|
|
289
|
+
# the repo root.
|
|
290
|
+
if link.is_dir() and not _is_pruned(name):
|
|
291
|
+
dir_node = RepoNode(
|
|
292
|
+
path=link,
|
|
293
|
+
link_path=display_dir / name,
|
|
294
|
+
name=name,
|
|
295
|
+
depth=node.depth + 1,
|
|
296
|
+
parent=node,
|
|
297
|
+
)
|
|
298
|
+
_walk_links_plain(dir_node, link, display_dir / name,
|
|
299
|
+
max_depth, visited)
|
|
300
|
+
if dir_node.children:
|
|
301
|
+
node.children.append(dir_node)
|
|
278
302
|
continue
|
|
279
303
|
target = link.resolve(strict=True)
|
|
280
304
|
except (OSError, RuntimeError):
|
|
@@ -318,6 +342,63 @@ def _walk_links(
|
|
|
318
342
|
node.children.sort(key=lambda c: c.name.lower())
|
|
319
343
|
|
|
320
344
|
|
|
345
|
+
def _walk_links_plain(
|
|
346
|
+
node: RepoNode,
|
|
347
|
+
real_dir: Path,
|
|
348
|
+
display_dir: Path,
|
|
349
|
+
max_depth: int,
|
|
350
|
+
visited: set[Path],
|
|
351
|
+
) -> None:
|
|
352
|
+
"""Look for symlinks inside a plain dir the first walk never recorded
|
|
353
|
+
(plain dirs inside a repo are skipped there). Records only the links
|
|
354
|
+
themselves as children of `node` — the plain dir stays invisible, so the
|
|
355
|
+
tree does not grow a directory per level of indirection.
|
|
356
|
+
|
|
357
|
+
Bounded to one level: the `.links/` layout is one dir of links; a full
|
|
358
|
+
recursive scan of a repo's working tree would be noise and cost.
|
|
359
|
+
"""
|
|
360
|
+
try:
|
|
361
|
+
entries = sorted(os.listdir(real_dir), key=lambda s: s.lower())
|
|
362
|
+
except OSError:
|
|
363
|
+
return
|
|
364
|
+
for name in entries:
|
|
365
|
+
if name == ".git":
|
|
366
|
+
continue
|
|
367
|
+
link = real_dir / name
|
|
368
|
+
try:
|
|
369
|
+
if not link.is_symlink():
|
|
370
|
+
continue
|
|
371
|
+
target = link.resolve(strict=True)
|
|
372
|
+
except (OSError, RuntimeError):
|
|
373
|
+
continue
|
|
374
|
+
if not target.is_dir():
|
|
375
|
+
continue
|
|
376
|
+
display = display_dir / name
|
|
377
|
+
already = target in visited
|
|
378
|
+
if not already:
|
|
379
|
+
visited.add(target)
|
|
380
|
+
try:
|
|
381
|
+
is_repo = _is_git_dir(target)
|
|
382
|
+
except OSError:
|
|
383
|
+
continue
|
|
384
|
+
child = RepoNode(
|
|
385
|
+
path=target,
|
|
386
|
+
link_path=display,
|
|
387
|
+
name=name,
|
|
388
|
+
depth=node.depth + 1,
|
|
389
|
+
parent=node,
|
|
390
|
+
is_symlink=True,
|
|
391
|
+
symlink_target=_readlink(link),
|
|
392
|
+
is_repo=is_repo and not already,
|
|
393
|
+
is_alias=already,
|
|
394
|
+
)
|
|
395
|
+
node.children.append(child)
|
|
396
|
+
if is_repo and not already:
|
|
397
|
+
_add_submodules(child, target, display, node.depth + 1, visited)
|
|
398
|
+
|
|
399
|
+
node.children.sort(key=lambda c: c.name.lower())
|
|
400
|
+
|
|
401
|
+
|
|
321
402
|
def _add_submodules(
|
|
322
403
|
parent: RepoNode,
|
|
323
404
|
repo_root: Path,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|