@pennyfarthing/core 7.8.1 → 7.8.4
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.
- package/README.md +1 -1
- package/package.json +2 -1
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +7 -6
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
- package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +25 -0
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
- package/pennyfarthing-dist/scripts/core/prime.sh +23 -0
- package/pennyfarthing-dist/scripts/core/run.sh +5 -5
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
- package/pennyfarthing-dist/scripts/git/release.sh +2 -2
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
- package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
- package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
- package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
- package/pennyfarthing_scripts/__init__.py +17 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bellmode_hook.py +154 -0
- package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
- package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/cli.py +131 -0
- package/pennyfarthing_scripts/brownfield/discover.py +753 -0
- package/pennyfarthing_scripts/common/__init__.py +49 -0
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +65 -0
- package/pennyfarthing_scripts/common/output.py +180 -0
- package/pennyfarthing_scripts/config.py +21 -0
- package/pennyfarthing_scripts/git/__init__.py +29 -0
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +439 -0
- package/pennyfarthing_scripts/git/status_all.py +310 -0
- package/pennyfarthing_scripts/hooks.py +455 -0
- package/pennyfarthing_scripts/jira/__init__.py +93 -0
- package/pennyfarthing_scripts/jira/__main__.py +10 -0
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
- package/pennyfarthing_scripts/jira/claim.py +211 -0
- package/pennyfarthing_scripts/jira/cli.py +150 -0
- package/pennyfarthing_scripts/jira/client.py +613 -0
- package/pennyfarthing_scripts/jira/epic.py +176 -0
- package/pennyfarthing_scripts/jira/story.py +219 -0
- package/pennyfarthing_scripts/jira/sync.py +350 -0
- package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
- package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
- package/pennyfarthing_scripts/jira_sync.py +36 -0
- package/pennyfarthing_scripts/jira_sync_story.py +30 -0
- package/pennyfarthing_scripts/output.py +37 -0
- package/pennyfarthing_scripts/preflight/__init__.py +17 -0
- package/pennyfarthing_scripts/preflight/__main__.py +10 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/cli.py +141 -0
- package/pennyfarthing_scripts/preflight/finish.py +382 -0
- package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
- package/pennyfarthing_scripts/prime/__init__.py +38 -0
- package/pennyfarthing_scripts/prime/__main__.py +8 -0
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +220 -0
- package/pennyfarthing_scripts/prime/loader.py +239 -0
- package/pennyfarthing_scripts/sprint/__init__.py +66 -0
- package/pennyfarthing_scripts/sprint/__main__.py +10 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +108 -0
- package/pennyfarthing_scripts/sprint/cli.py +124 -0
- package/pennyfarthing_scripts/sprint/loader.py +193 -0
- package/pennyfarthing_scripts/sprint/status.py +122 -0
- package/pennyfarthing_scripts/sprint/validator.py +405 -0
- package/pennyfarthing_scripts/sprint/work.py +192 -0
- package/pennyfarthing_scripts/story/__init__.py +67 -0
- package/pennyfarthing_scripts/story/__main__.py +10 -0
- package/pennyfarthing_scripts/story/cli.py +105 -0
- package/pennyfarthing_scripts/story/create.py +167 -0
- package/pennyfarthing_scripts/story/size.py +113 -0
- package/pennyfarthing_scripts/story/template.py +151 -0
- package/pennyfarthing_scripts/swebench.py +216 -0
- package/pennyfarthing_scripts/tests/__init__.py +1 -0
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/conftest.py +106 -0
- package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
- package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
- package/pennyfarthing_scripts/tests/test_common.py +180 -0
- package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
- package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
- package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
- package/pennyfarthing_scripts/tests/test_prime.py +397 -0
- package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
- package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
- package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
- package/pennyfarthing_scripts/welcome_hook.py +157 -0
- package/pennyfarthing_scripts/workflow.py +183 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git status for all repos - async parallel execution.
|
|
3
|
+
|
|
4
|
+
Story: MSSCI-12402 - Port git utility scripts to Python
|
|
5
|
+
|
|
6
|
+
Replaces: pennyfarthing-dist/scripts/git/git-status-all.sh
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- asyncio.gather for true parallel git operations
|
|
10
|
+
- Structured RepoStatus dataclass for programmatic access
|
|
11
|
+
- Both brief and full output formatting
|
|
12
|
+
- Cross-platform compatible
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Sequence
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class RepoStatus:
|
|
23
|
+
"""Status information for a single repository."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
path: Path
|
|
27
|
+
branch: str
|
|
28
|
+
changes: list[str] # List of changed files (git status --short lines)
|
|
29
|
+
unpushed_commits: list[str] # List of unpushed commit messages
|
|
30
|
+
error: str | None = None # Error message if status check failed
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_clean(self) -> bool:
|
|
34
|
+
"""Return True if repo has no uncommitted changes."""
|
|
35
|
+
return len(self.changes) == 0
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def has_unpushed(self) -> bool:
|
|
39
|
+
"""Return True if repo has unpushed commits."""
|
|
40
|
+
return len(self.unpushed_commits) > 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _run_git_command(args: list[str], cwd: Path) -> tuple[str, str, int]:
|
|
44
|
+
"""Run a git command asynchronously.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
args: Git command arguments (without 'git')
|
|
48
|
+
cwd: Working directory for the command
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (stdout, stderr, return_code)
|
|
52
|
+
"""
|
|
53
|
+
proc = await asyncio.create_subprocess_exec(
|
|
54
|
+
"git",
|
|
55
|
+
*args,
|
|
56
|
+
cwd=cwd,
|
|
57
|
+
stdout=asyncio.subprocess.PIPE,
|
|
58
|
+
stderr=asyncio.subprocess.PIPE,
|
|
59
|
+
)
|
|
60
|
+
stdout, stderr = await proc.communicate()
|
|
61
|
+
return (
|
|
62
|
+
stdout.decode("utf-8", errors="replace").strip(),
|
|
63
|
+
stderr.decode("utf-8", errors="replace").strip(),
|
|
64
|
+
proc.returncode or 0,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def get_repo_status(name: str, path: Path) -> RepoStatus:
|
|
69
|
+
"""Get git status for a single repository.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Display name for the repo
|
|
73
|
+
path: Path to the repository
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
RepoStatus with current branch, changes, and unpushed commits
|
|
77
|
+
"""
|
|
78
|
+
# Check if path exists
|
|
79
|
+
if not path.exists():
|
|
80
|
+
return RepoStatus(
|
|
81
|
+
name=name,
|
|
82
|
+
path=path,
|
|
83
|
+
branch="",
|
|
84
|
+
changes=[],
|
|
85
|
+
unpushed_commits=[],
|
|
86
|
+
error=f"Path not found: {path}",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Check if it's a git repo
|
|
90
|
+
git_dir = path / ".git"
|
|
91
|
+
if not git_dir.exists() and not (path / "..").joinpath(".git").exists():
|
|
92
|
+
# Also check if path itself is a git dir (bare repo or worktree)
|
|
93
|
+
try:
|
|
94
|
+
_, _, rc = await _run_git_command(["rev-parse", "--git-dir"], path)
|
|
95
|
+
if rc != 0:
|
|
96
|
+
return RepoStatus(
|
|
97
|
+
name=name,
|
|
98
|
+
path=path,
|
|
99
|
+
branch="",
|
|
100
|
+
changes=[],
|
|
101
|
+
unpushed_commits=[],
|
|
102
|
+
error=f"Not a git repository: {path}",
|
|
103
|
+
)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return RepoStatus(
|
|
106
|
+
name=name,
|
|
107
|
+
path=path,
|
|
108
|
+
branch="",
|
|
109
|
+
changes=[],
|
|
110
|
+
unpushed_commits=[],
|
|
111
|
+
error=f"Git command failed: {e}",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
# Get current branch
|
|
116
|
+
branch_out, _, branch_rc = await _run_git_command(
|
|
117
|
+
["branch", "--show-current"], path
|
|
118
|
+
)
|
|
119
|
+
if branch_rc != 0 or not branch_out:
|
|
120
|
+
# Might be in detached HEAD state
|
|
121
|
+
branch_out = "detached"
|
|
122
|
+
|
|
123
|
+
# Get status (uncommitted changes)
|
|
124
|
+
status_out, _, _ = await _run_git_command(["status", "--short"], path)
|
|
125
|
+
changes = [line for line in status_out.split("\n") if line.strip()]
|
|
126
|
+
|
|
127
|
+
# Get unpushed commits (comparing to origin/develop)
|
|
128
|
+
unpushed_out, _, unpushed_rc = await _run_git_command(
|
|
129
|
+
["log", "origin/develop..HEAD", "--oneline"], path
|
|
130
|
+
)
|
|
131
|
+
if unpushed_rc == 0 and unpushed_out:
|
|
132
|
+
unpushed_commits = [
|
|
133
|
+
line for line in unpushed_out.split("\n") if line.strip()
|
|
134
|
+
]
|
|
135
|
+
else:
|
|
136
|
+
unpushed_commits = []
|
|
137
|
+
|
|
138
|
+
return RepoStatus(
|
|
139
|
+
name=name,
|
|
140
|
+
path=path,
|
|
141
|
+
branch=branch_out,
|
|
142
|
+
changes=changes,
|
|
143
|
+
unpushed_commits=unpushed_commits,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
return RepoStatus(
|
|
148
|
+
name=name,
|
|
149
|
+
path=path,
|
|
150
|
+
branch="",
|
|
151
|
+
changes=[],
|
|
152
|
+
unpushed_commits=[],
|
|
153
|
+
error=f"Error getting status: {e}",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def get_all_repo_status(repos: Sequence[tuple[str, Path]]) -> list[RepoStatus]:
|
|
158
|
+
"""Get git status for all repos in parallel using asyncio.gather.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
repos: Sequence of (name, path) tuples for each repo
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of RepoStatus objects in same order as input
|
|
165
|
+
"""
|
|
166
|
+
if not repos:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
tasks = [get_repo_status(name, path) for name, path in repos]
|
|
170
|
+
results = await asyncio.gather(*tasks, return_exceptions=False)
|
|
171
|
+
return list(results)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def format_status_brief(statuses: Sequence[RepoStatus]) -> str:
|
|
175
|
+
"""Format repo statuses as brief one-line-per-repo output.
|
|
176
|
+
|
|
177
|
+
Format: "repo_name: branch_name [M] [↑N]"
|
|
178
|
+
- M = has modifications
|
|
179
|
+
- ↑N = N unpushed commits
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
statuses: Sequence of RepoStatus objects
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Multi-line string with brief status for each repo
|
|
186
|
+
"""
|
|
187
|
+
lines = []
|
|
188
|
+
for status in statuses:
|
|
189
|
+
indicators = []
|
|
190
|
+
|
|
191
|
+
# Modification indicator
|
|
192
|
+
if not status.is_clean:
|
|
193
|
+
indicators.append("M")
|
|
194
|
+
else:
|
|
195
|
+
indicators.append("✓")
|
|
196
|
+
|
|
197
|
+
# Unpushed indicator
|
|
198
|
+
if status.has_unpushed:
|
|
199
|
+
indicators.append(f"↑{len(status.unpushed_commits)}")
|
|
200
|
+
|
|
201
|
+
indicator_str = " ".join(indicators)
|
|
202
|
+
lines.append(f"{status.name}: {status.branch} {indicator_str}")
|
|
203
|
+
|
|
204
|
+
return "\n".join(lines)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def format_status_full(statuses: Sequence[RepoStatus]) -> str:
|
|
208
|
+
"""Format repo statuses as full detailed output.
|
|
209
|
+
|
|
210
|
+
Shows branch, changes (up to 10), and unpushed commits (up to 5).
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
statuses: Sequence of RepoStatus objects
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Multi-line string with detailed status for each repo
|
|
217
|
+
"""
|
|
218
|
+
sections = []
|
|
219
|
+
|
|
220
|
+
for status in statuses:
|
|
221
|
+
lines = []
|
|
222
|
+
lines.append(f"=== {status.name} ===")
|
|
223
|
+
lines.append(f"Branch: {status.branch}")
|
|
224
|
+
|
|
225
|
+
if status.error:
|
|
226
|
+
lines.append(f"Error: {status.error}")
|
|
227
|
+
elif status.changes:
|
|
228
|
+
lines.append("Changes:")
|
|
229
|
+
for change in status.changes[:10]:
|
|
230
|
+
lines.append(f" {change}")
|
|
231
|
+
if len(status.changes) > 10:
|
|
232
|
+
lines.append(f" ... and {len(status.changes) - 10} more")
|
|
233
|
+
else:
|
|
234
|
+
lines.append("Clean")
|
|
235
|
+
|
|
236
|
+
if status.unpushed_commits:
|
|
237
|
+
lines.append(f"Unpushed ({len(status.unpushed_commits)}):")
|
|
238
|
+
for commit in status.unpushed_commits[:5]:
|
|
239
|
+
lines.append(f" {commit}")
|
|
240
|
+
if len(status.unpushed_commits) > 5:
|
|
241
|
+
lines.append(f" ... and {len(status.unpushed_commits) - 5} more")
|
|
242
|
+
|
|
243
|
+
lines.append("")
|
|
244
|
+
sections.append("\n".join(lines))
|
|
245
|
+
|
|
246
|
+
return "\n".join(sections)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def format_summary(statuses: Sequence[RepoStatus]) -> str:
|
|
250
|
+
"""Format summary of all repo statuses.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
statuses: Sequence of RepoStatus objects
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Summary string with total changes and unpushed counts
|
|
257
|
+
"""
|
|
258
|
+
total_changes = sum(len(s.changes) for s in statuses)
|
|
259
|
+
total_unpushed = sum(len(s.unpushed_commits) for s in statuses)
|
|
260
|
+
|
|
261
|
+
if total_changes == 0 and total_unpushed == 0:
|
|
262
|
+
return "✅ All repos clean and pushed"
|
|
263
|
+
|
|
264
|
+
parts = []
|
|
265
|
+
if total_changes > 0:
|
|
266
|
+
parts.append(f"{total_changes} uncommitted change(s)")
|
|
267
|
+
if total_unpushed > 0:
|
|
268
|
+
parts.append(f"{total_unpushed} unpushed commit(s)")
|
|
269
|
+
|
|
270
|
+
return " | ".join(parts)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
async def main(brief: bool = False) -> int:
|
|
274
|
+
"""CLI entry point for git-status-all.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
brief: If True, use brief output format
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
0 if all repos clean, 1 if any have changes/unpushed
|
|
281
|
+
"""
|
|
282
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
283
|
+
|
|
284
|
+
# For now, just check the current project
|
|
285
|
+
project_root = get_project_root()
|
|
286
|
+
repos = [("pennyfarthing", project_root)]
|
|
287
|
+
|
|
288
|
+
statuses = await get_all_repo_status(repos)
|
|
289
|
+
|
|
290
|
+
if brief:
|
|
291
|
+
print(format_status_brief(statuses))
|
|
292
|
+
else:
|
|
293
|
+
print("━" * 40)
|
|
294
|
+
print(" Git Status - All Repos")
|
|
295
|
+
print("━" * 40)
|
|
296
|
+
print()
|
|
297
|
+
print(format_status_full(statuses))
|
|
298
|
+
print("━" * 40)
|
|
299
|
+
print(format_summary(statuses))
|
|
300
|
+
|
|
301
|
+
# Return 1 if any repo has changes or unpushed
|
|
302
|
+
has_issues = any(not s.is_clean or s.has_unpushed for s in statuses)
|
|
303
|
+
return 1 if has_issues else 0
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
import sys
|
|
308
|
+
|
|
309
|
+
brief_mode = "--brief" in sys.argv or "-b" in sys.argv
|
|
310
|
+
sys.exit(asyncio.run(main(brief=brief_mode)))
|