elspais 0.11.1__py3-none-any.whl → 0.11.2__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.
- elspais/__init__.py +1 -1
- elspais/cli.py +29 -10
- elspais/commands/analyze.py +5 -6
- elspais/commands/changed.py +2 -6
- elspais/commands/config_cmd.py +4 -4
- elspais/commands/edit.py +32 -36
- elspais/commands/hash_cmd.py +24 -18
- elspais/commands/index.py +8 -7
- elspais/commands/init.py +4 -4
- elspais/commands/reformat_cmd.py +32 -43
- elspais/commands/rules_cmd.py +6 -2
- elspais/commands/trace.py +23 -19
- elspais/commands/validate.py +8 -10
- elspais/config/defaults.py +7 -1
- elspais/core/content_rules.py +0 -1
- elspais/core/git.py +4 -10
- elspais/core/parser.py +55 -56
- elspais/core/patterns.py +2 -6
- elspais/core/rules.py +10 -15
- elspais/mcp/__init__.py +2 -0
- elspais/mcp/context.py +1 -0
- elspais/mcp/serializers.py +1 -1
- elspais/mcp/server.py +54 -39
- elspais/reformat/__init__.py +13 -13
- elspais/reformat/detector.py +9 -16
- elspais/reformat/hierarchy.py +8 -7
- elspais/reformat/line_breaks.py +36 -38
- elspais/reformat/prompts.py +22 -12
- elspais/reformat/transformer.py +43 -41
- elspais/sponsors/__init__.py +0 -2
- elspais/testing/__init__.py +1 -1
- elspais/testing/result_parser.py +25 -21
- elspais/trace_view/__init__.py +4 -3
- elspais/trace_view/coverage.py +5 -5
- elspais/trace_view/generators/__init__.py +1 -1
- elspais/trace_view/generators/base.py +17 -12
- elspais/trace_view/generators/csv.py +2 -6
- elspais/trace_view/generators/markdown.py +3 -8
- elspais/trace_view/html/__init__.py +4 -2
- elspais/trace_view/html/generator.py +423 -289
- elspais/trace_view/models.py +25 -0
- elspais/trace_view/review/__init__.py +21 -18
- elspais/trace_view/review/branches.py +114 -121
- elspais/trace_view/review/models.py +232 -237
- elspais/trace_view/review/position.py +53 -71
- elspais/trace_view/review/server.py +264 -288
- elspais/trace_view/review/status.py +43 -58
- elspais/trace_view/review/storage.py +48 -72
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/METADATA +1 -1
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/RECORD +53 -53
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/WHEEL +0 -0
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -23,18 +23,18 @@ from datetime import datetime, timezone
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import List, Optional, Tuple
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
# =============================================================================
|
|
28
27
|
# Constants
|
|
29
28
|
# =============================================================================
|
|
30
29
|
|
|
31
|
-
REVIEW_BRANCH_PREFIX =
|
|
30
|
+
REVIEW_BRANCH_PREFIX = "reviews/"
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
# =============================================================================
|
|
35
34
|
# Data Classes (REQ-tv-d00013)
|
|
36
35
|
# =============================================================================
|
|
37
36
|
|
|
37
|
+
|
|
38
38
|
@dataclass
|
|
39
39
|
class BranchInfo:
|
|
40
40
|
"""
|
|
@@ -42,13 +42,14 @@ class BranchInfo:
|
|
|
42
42
|
|
|
43
43
|
REQ-tv-d00013: Branch info data class for listing and cleanup operations.
|
|
44
44
|
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
|
|
46
|
+
name: str # Full branch name: reviews/{pkg}/{user}
|
|
47
|
+
package_id: str # Package identifier
|
|
48
|
+
username: str # User who owns the branch
|
|
49
|
+
last_commit_date: datetime # Date of last commit on branch
|
|
50
|
+
is_current: bool # True if this is the current branch
|
|
51
|
+
has_remote: bool # True if remote tracking branch exists
|
|
52
|
+
is_merged: bool # True if merged into main
|
|
52
53
|
|
|
53
54
|
@property
|
|
54
55
|
def age_days(self) -> int:
|
|
@@ -67,6 +68,7 @@ class BranchInfo:
|
|
|
67
68
|
# Branch Naming (REQ-tv-d00013-A, B)
|
|
68
69
|
# =============================================================================
|
|
69
70
|
|
|
71
|
+
|
|
70
72
|
def get_review_branch_name(package_id: str, user: str) -> str:
|
|
71
73
|
"""
|
|
72
74
|
Generate a review branch name from package and user.
|
|
@@ -101,11 +103,11 @@ def _sanitize_branch_name(name: str) -> str:
|
|
|
101
103
|
Replaces spaces with hyphens and removes invalid characters.
|
|
102
104
|
"""
|
|
103
105
|
# Replace spaces with hyphens
|
|
104
|
-
name = name.replace(
|
|
106
|
+
name = name.replace(" ", "-")
|
|
105
107
|
# Remove invalid characters (keep alphanumeric, hyphen, underscore)
|
|
106
|
-
name = re.sub(r
|
|
108
|
+
name = re.sub(r"[^a-zA-Z0-9_-]", "", name)
|
|
107
109
|
# Remove leading/trailing hyphens
|
|
108
|
-
name = name.strip(
|
|
110
|
+
name = name.strip("-")
|
|
109
111
|
# Convert to lowercase
|
|
110
112
|
return name.lower()
|
|
111
113
|
|
|
@@ -114,6 +116,7 @@ def _sanitize_branch_name(name: str) -> str:
|
|
|
114
116
|
# Branch Parsing (REQ-tv-d00013-C, D)
|
|
115
117
|
# =============================================================================
|
|
116
118
|
|
|
119
|
+
|
|
117
120
|
def parse_review_branch_name(branch_name: str) -> Optional[Tuple[str, str]]:
|
|
118
121
|
"""
|
|
119
122
|
Parse a review branch name into (package_id, user).
|
|
@@ -139,8 +142,8 @@ def parse_review_branch_name(branch_name: str) -> Optional[Tuple[str, str]]:
|
|
|
139
142
|
return None
|
|
140
143
|
|
|
141
144
|
# Remove prefix
|
|
142
|
-
remainder = branch_name[len(REVIEW_BRANCH_PREFIX):]
|
|
143
|
-
parts = remainder.split(
|
|
145
|
+
remainder = branch_name[len(REVIEW_BRANCH_PREFIX) :]
|
|
146
|
+
parts = remainder.split("/", 1)
|
|
144
147
|
|
|
145
148
|
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
146
149
|
return None
|
|
@@ -175,8 +178,8 @@ def is_review_branch(branch_name: str) -> bool:
|
|
|
175
178
|
if not branch_name.startswith(REVIEW_BRANCH_PREFIX):
|
|
176
179
|
return False
|
|
177
180
|
|
|
178
|
-
remainder = branch_name[len(REVIEW_BRANCH_PREFIX):]
|
|
179
|
-
parts = remainder.split(
|
|
181
|
+
remainder = branch_name[len(REVIEW_BRANCH_PREFIX) :]
|
|
182
|
+
parts = remainder.split("/", 1)
|
|
180
183
|
|
|
181
184
|
# Must have both package and user
|
|
182
185
|
return len(parts) == 2 and bool(parts[0]) and bool(parts[1])
|
|
@@ -186,8 +189,8 @@ def is_review_branch(branch_name: str) -> bool:
|
|
|
186
189
|
# Git Utilities
|
|
187
190
|
# =============================================================================
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
|
|
193
|
+
def _run_git(repo_root: Path, args: List[str], check: bool = False) -> subprocess.CompletedProcess:
|
|
191
194
|
"""
|
|
192
195
|
Run a git command in the repository.
|
|
193
196
|
|
|
@@ -201,19 +204,12 @@ def _run_git(repo_root: Path, args: List[str],
|
|
|
201
204
|
"""
|
|
202
205
|
try:
|
|
203
206
|
return subprocess.run(
|
|
204
|
-
[
|
|
205
|
-
cwd=repo_root,
|
|
206
|
-
capture_output=True,
|
|
207
|
-
text=True,
|
|
208
|
-
check=check
|
|
207
|
+
["git"] + args, cwd=repo_root, capture_output=True, text=True, check=check
|
|
209
208
|
)
|
|
210
209
|
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
211
210
|
# Return a fake failed result
|
|
212
211
|
return subprocess.CompletedProcess(
|
|
213
|
-
args=[
|
|
214
|
-
returncode=1,
|
|
215
|
-
stdout='',
|
|
216
|
-
stderr='Error running git'
|
|
212
|
+
args=["git"] + args, returncode=1, stdout="", stderr="Error running git"
|
|
217
213
|
)
|
|
218
214
|
|
|
219
215
|
|
|
@@ -227,7 +223,7 @@ def get_current_branch(repo_root: Path) -> Optional[str]:
|
|
|
227
223
|
Returns:
|
|
228
224
|
Branch name or None if not in a git repo
|
|
229
225
|
"""
|
|
230
|
-
result = _run_git(repo_root, [
|
|
226
|
+
result = _run_git(repo_root, ["rev-parse", "--abbrev-ref", "HEAD"])
|
|
231
227
|
if result.returncode != 0:
|
|
232
228
|
return None
|
|
233
229
|
return result.stdout.strip()
|
|
@@ -243,17 +239,18 @@ def get_remote_name(repo_root: Path) -> Optional[str]:
|
|
|
243
239
|
Returns:
|
|
244
240
|
Remote name or None if no remotes configured
|
|
245
241
|
"""
|
|
246
|
-
result = _run_git(repo_root, [
|
|
242
|
+
result = _run_git(repo_root, ["remote"])
|
|
247
243
|
if result.returncode != 0 or not result.stdout.strip():
|
|
248
244
|
return None
|
|
249
245
|
# Return first remote
|
|
250
|
-
return result.stdout.strip().split(
|
|
246
|
+
return result.stdout.strip().split("\n")[0]
|
|
251
247
|
|
|
252
248
|
|
|
253
249
|
# =============================================================================
|
|
254
250
|
# Git Audit Trail (REQ-d00098)
|
|
255
251
|
# =============================================================================
|
|
256
252
|
|
|
253
|
+
|
|
257
254
|
def get_head_commit_hash(repo_root: Path) -> Optional[str]:
|
|
258
255
|
"""
|
|
259
256
|
Get the current HEAD commit hash (full 40 characters).
|
|
@@ -267,7 +264,7 @@ def get_head_commit_hash(repo_root: Path) -> Optional[str]:
|
|
|
267
264
|
Returns:
|
|
268
265
|
Full commit hash (40 chars) or None if not in a git repo
|
|
269
266
|
"""
|
|
270
|
-
result = _run_git(repo_root, [
|
|
267
|
+
result = _run_git(repo_root, ["rev-parse", "HEAD"])
|
|
271
268
|
if result.returncode != 0:
|
|
272
269
|
return None
|
|
273
270
|
return result.stdout.strip()
|
|
@@ -284,7 +281,7 @@ def get_short_commit_hash(repo_root: Path, length: int = 7) -> Optional[str]:
|
|
|
284
281
|
Returns:
|
|
285
282
|
Short commit hash or None if not in a git repo
|
|
286
283
|
"""
|
|
287
|
-
result = _run_git(repo_root, [
|
|
284
|
+
result = _run_git(repo_root, ["rev-parse", f"--short={length}", "HEAD"])
|
|
288
285
|
if result.returncode != 0:
|
|
289
286
|
return None
|
|
290
287
|
return result.stdout.strip()
|
|
@@ -303,8 +300,8 @@ def get_git_context(repo_root: Path) -> dict:
|
|
|
303
300
|
Dictionary with 'branchName' and 'commitHash' keys (values may be None)
|
|
304
301
|
"""
|
|
305
302
|
return {
|
|
306
|
-
|
|
307
|
-
|
|
303
|
+
"branchName": get_current_branch(repo_root),
|
|
304
|
+
"commitHash": get_head_commit_hash(repo_root),
|
|
308
305
|
}
|
|
309
306
|
|
|
310
307
|
|
|
@@ -324,8 +321,8 @@ def commit_exists(repo_root: Path, commit_hash: str) -> bool:
|
|
|
324
321
|
Returns:
|
|
325
322
|
True if commit exists, False otherwise
|
|
326
323
|
"""
|
|
327
|
-
result = _run_git(repo_root, [
|
|
328
|
-
return result.returncode == 0 and result.stdout.strip() ==
|
|
324
|
+
result = _run_git(repo_root, ["cat-file", "-t", commit_hash])
|
|
325
|
+
return result.returncode == 0 and result.stdout.strip() == "commit"
|
|
329
326
|
|
|
330
327
|
|
|
331
328
|
def branch_exists(repo_root: Path, branch_name: str) -> bool:
|
|
@@ -339,12 +336,11 @@ def branch_exists(repo_root: Path, branch_name: str) -> bool:
|
|
|
339
336
|
Returns:
|
|
340
337
|
True if branch exists locally
|
|
341
338
|
"""
|
|
342
|
-
result = _run_git(repo_root, [
|
|
339
|
+
result = _run_git(repo_root, ["rev-parse", "--verify", f"refs/heads/{branch_name}"])
|
|
343
340
|
return result.returncode == 0
|
|
344
341
|
|
|
345
342
|
|
|
346
|
-
def remote_branch_exists(repo_root: Path, branch_name: str,
|
|
347
|
-
remote: str = 'origin') -> bool:
|
|
343
|
+
def remote_branch_exists(repo_root: Path, branch_name: str, remote: str = "origin") -> bool:
|
|
348
344
|
"""
|
|
349
345
|
Check if a remote branch exists.
|
|
350
346
|
|
|
@@ -356,7 +352,7 @@ def remote_branch_exists(repo_root: Path, branch_name: str,
|
|
|
356
352
|
Returns:
|
|
357
353
|
True if branch exists on remote
|
|
358
354
|
"""
|
|
359
|
-
result = _run_git(repo_root, [
|
|
355
|
+
result = _run_git(repo_root, ["rev-parse", "--verify", f"refs/remotes/{remote}/{branch_name}"])
|
|
360
356
|
return result.returncode == 0
|
|
361
357
|
|
|
362
358
|
|
|
@@ -364,6 +360,7 @@ def remote_branch_exists(repo_root: Path, branch_name: str,
|
|
|
364
360
|
# Branch Metadata (REQ-tv-d00013 Service Layer)
|
|
365
361
|
# =============================================================================
|
|
366
362
|
|
|
363
|
+
|
|
367
364
|
def get_branch_last_commit_date(repo_root: Path, branch_name: str) -> Optional[datetime]:
|
|
368
365
|
"""
|
|
369
366
|
Get the date of the last commit on a branch.
|
|
@@ -382,9 +379,7 @@ def get_branch_last_commit_date(repo_root: Path, branch_name: str) -> Optional[d
|
|
|
382
379
|
datetime.datetime(2025, 1, 8, 12, 30, 45, tzinfo=timezone.utc)
|
|
383
380
|
"""
|
|
384
381
|
# Use git log to get the commit date in ISO format
|
|
385
|
-
result = _run_git(repo_root, [
|
|
386
|
-
'log', '-1', '--format=%cI', branch_name
|
|
387
|
-
])
|
|
382
|
+
result = _run_git(repo_root, ["log", "-1", "--format=%cI", branch_name])
|
|
388
383
|
if result.returncode != 0 or not result.stdout.strip():
|
|
389
384
|
return None
|
|
390
385
|
|
|
@@ -400,8 +395,7 @@ def get_branch_last_commit_date(repo_root: Path, branch_name: str) -> Optional[d
|
|
|
400
395
|
return None
|
|
401
396
|
|
|
402
397
|
|
|
403
|
-
def is_branch_merged(repo_root: Path, branch_name: str,
|
|
404
|
-
target_branch: str = 'main') -> bool:
|
|
398
|
+
def is_branch_merged(repo_root: Path, branch_name: str, target_branch: str = "main") -> bool:
|
|
405
399
|
"""
|
|
406
400
|
Check if a branch has been merged into the target branch.
|
|
407
401
|
|
|
@@ -422,22 +416,21 @@ def is_branch_merged(repo_root: Path, branch_name: str,
|
|
|
422
416
|
False # bob's branch not merged to develop
|
|
423
417
|
"""
|
|
424
418
|
# git branch --merged <target> lists branches merged into target
|
|
425
|
-
result = _run_git(repo_root, [
|
|
419
|
+
result = _run_git(repo_root, ["branch", "--merged", target_branch])
|
|
426
420
|
if result.returncode != 0:
|
|
427
421
|
return False
|
|
428
422
|
|
|
429
423
|
# Parse branch list and check if our branch is in it
|
|
430
424
|
merged_branches = []
|
|
431
|
-
for line in result.stdout.strip().split(
|
|
432
|
-
branch = line.strip().lstrip(
|
|
425
|
+
for line in result.stdout.strip().split("\n"):
|
|
426
|
+
branch = line.strip().lstrip("* ")
|
|
433
427
|
if branch:
|
|
434
428
|
merged_branches.append(branch)
|
|
435
429
|
|
|
436
430
|
return branch_name in merged_branches
|
|
437
431
|
|
|
438
432
|
|
|
439
|
-
def has_unpushed_commits(repo_root: Path, branch_name: str,
|
|
440
|
-
remote: str = 'origin') -> bool:
|
|
433
|
+
def has_unpushed_commits(repo_root: Path, branch_name: str, remote: str = "origin") -> bool:
|
|
441
434
|
"""
|
|
442
435
|
Check if a branch has commits not pushed to remote.
|
|
443
436
|
|
|
@@ -460,10 +453,7 @@ def has_unpushed_commits(repo_root: Path, branch_name: str,
|
|
|
460
453
|
return True # Remote exists but branch not pushed
|
|
461
454
|
|
|
462
455
|
# Compare local and remote
|
|
463
|
-
result = _run_git(repo_root, [
|
|
464
|
-
'rev-list', '--count',
|
|
465
|
-
f'{remote}/{branch_name}..{branch_name}'
|
|
466
|
-
])
|
|
456
|
+
result = _run_git(repo_root, ["rev-list", "--count", f"{remote}/{branch_name}..{branch_name}"])
|
|
467
457
|
if result.returncode != 0:
|
|
468
458
|
return True # Assume unpushed if we can't check
|
|
469
459
|
|
|
@@ -513,7 +503,7 @@ def get_branch_info(repo_root: Path, branch_name: str) -> Optional[BranchInfo]:
|
|
|
513
503
|
|
|
514
504
|
# Check if current branch
|
|
515
505
|
current = get_current_branch(repo_root)
|
|
516
|
-
is_current =
|
|
506
|
+
is_current = current == branch_name
|
|
517
507
|
|
|
518
508
|
# Check remote existence
|
|
519
509
|
has_remote = remote_branch_exists(repo_root, branch_name)
|
|
@@ -528,7 +518,7 @@ def get_branch_info(repo_root: Path, branch_name: str) -> Optional[BranchInfo]:
|
|
|
528
518
|
last_commit_date=last_commit,
|
|
529
519
|
is_current=is_current,
|
|
530
520
|
has_remote=has_remote,
|
|
531
|
-
is_merged=is_merged
|
|
521
|
+
is_merged=is_merged,
|
|
532
522
|
)
|
|
533
523
|
|
|
534
524
|
|
|
@@ -536,6 +526,7 @@ def get_branch_info(repo_root: Path, branch_name: str) -> Optional[BranchInfo]:
|
|
|
536
526
|
# Package Context (REQ-tv-d00013-F)
|
|
537
527
|
# =============================================================================
|
|
538
528
|
|
|
529
|
+
|
|
539
530
|
def get_current_package_context(repo_root: Path) -> Tuple[Optional[str], Optional[str]]:
|
|
540
531
|
"""
|
|
541
532
|
Get current (package_id, user) from branch name.
|
|
@@ -569,6 +560,7 @@ def get_current_package_context(repo_root: Path) -> Tuple[Optional[str], Optiona
|
|
|
569
560
|
# Branch Discovery (REQ-tv-d00013-E)
|
|
570
561
|
# =============================================================================
|
|
571
562
|
|
|
563
|
+
|
|
572
564
|
def list_package_branches(repo_root: Path, package_id: str) -> List[str]:
|
|
573
565
|
"""
|
|
574
566
|
List all local review branches for a specific package.
|
|
@@ -603,21 +595,20 @@ def _list_branches_by_pattern(repo_root: Path, pattern: str) -> List[str]:
|
|
|
603
595
|
Returns:
|
|
604
596
|
List of matching branch names
|
|
605
597
|
"""
|
|
606
|
-
result = _run_git(repo_root, [
|
|
598
|
+
result = _run_git(repo_root, ["branch", "--list", pattern])
|
|
607
599
|
if result.returncode != 0:
|
|
608
600
|
return []
|
|
609
601
|
|
|
610
602
|
branches = []
|
|
611
|
-
for line in result.stdout.strip().split(
|
|
612
|
-
branch = line.strip().lstrip(
|
|
603
|
+
for line in result.stdout.strip().split("\n"):
|
|
604
|
+
branch = line.strip().lstrip("* ")
|
|
613
605
|
if branch and is_review_branch(branch):
|
|
614
606
|
branches.append(branch)
|
|
615
607
|
|
|
616
608
|
return branches
|
|
617
609
|
|
|
618
610
|
|
|
619
|
-
def list_local_review_branches(repo_root: Path,
|
|
620
|
-
user: Optional[str] = None) -> List[str]:
|
|
611
|
+
def list_local_review_branches(repo_root: Path, user: Optional[str] = None) -> List[str]:
|
|
621
612
|
"""
|
|
622
613
|
List all local review branches.
|
|
623
614
|
|
|
@@ -628,14 +619,14 @@ def list_local_review_branches(repo_root: Path,
|
|
|
628
619
|
Returns:
|
|
629
620
|
List of branch names
|
|
630
621
|
"""
|
|
631
|
-
result = _run_git(repo_root, [
|
|
622
|
+
result = _run_git(repo_root, ["branch", "--list", "reviews/*"])
|
|
632
623
|
if result.returncode != 0:
|
|
633
624
|
return []
|
|
634
625
|
|
|
635
626
|
branches = []
|
|
636
|
-
for line in result.stdout.strip().split(
|
|
627
|
+
for line in result.stdout.strip().split("\n"):
|
|
637
628
|
# Remove leading * and whitespace
|
|
638
|
-
branch = line.strip().lstrip(
|
|
629
|
+
branch = line.strip().lstrip("* ")
|
|
639
630
|
if branch and is_review_branch(branch):
|
|
640
631
|
if user is None:
|
|
641
632
|
branches.append(branch)
|
|
@@ -652,6 +643,7 @@ def list_local_review_branches(repo_root: Path,
|
|
|
652
643
|
# Branch Operations
|
|
653
644
|
# =============================================================================
|
|
654
645
|
|
|
646
|
+
|
|
655
647
|
def create_review_branch(repo_root: Path, package_id: str, user: str) -> str:
|
|
656
648
|
"""
|
|
657
649
|
Create a new review branch.
|
|
@@ -673,7 +665,7 @@ def create_review_branch(repo_root: Path, package_id: str, user: str) -> str:
|
|
|
673
665
|
if branch_exists(repo_root, branch_name):
|
|
674
666
|
raise ValueError(f"Branch already exists: {branch_name}")
|
|
675
667
|
|
|
676
|
-
result = _run_git(repo_root, [
|
|
668
|
+
result = _run_git(repo_root, ["branch", branch_name])
|
|
677
669
|
if result.returncode != 0:
|
|
678
670
|
raise RuntimeError(f"Failed to create branch: {result.stderr}")
|
|
679
671
|
|
|
@@ -697,7 +689,7 @@ def checkout_review_branch(repo_root: Path, package_id: str, user: str) -> bool:
|
|
|
697
689
|
if not branch_exists(repo_root, branch_name):
|
|
698
690
|
return False
|
|
699
691
|
|
|
700
|
-
result = _run_git(repo_root, [
|
|
692
|
+
result = _run_git(repo_root, ["checkout", branch_name])
|
|
701
693
|
return result.returncode == 0
|
|
702
694
|
|
|
703
695
|
|
|
@@ -705,6 +697,7 @@ def checkout_review_branch(repo_root: Path, package_id: str, user: str) -> bool:
|
|
|
705
697
|
# Change Detection
|
|
706
698
|
# =============================================================================
|
|
707
699
|
|
|
700
|
+
|
|
708
701
|
def has_uncommitted_changes(repo_root: Path) -> bool:
|
|
709
702
|
"""
|
|
710
703
|
Check if there are uncommitted changes.
|
|
@@ -717,7 +710,7 @@ def has_uncommitted_changes(repo_root: Path) -> bool:
|
|
|
717
710
|
Returns:
|
|
718
711
|
True if there are uncommitted changes (staged or unstaged)
|
|
719
712
|
"""
|
|
720
|
-
result = _run_git(repo_root, [
|
|
713
|
+
result = _run_git(repo_root, ["status", "--porcelain"])
|
|
721
714
|
return bool(result.stdout.strip())
|
|
722
715
|
|
|
723
716
|
|
|
@@ -731,11 +724,11 @@ def has_reviews_changes(repo_root: Path) -> bool:
|
|
|
731
724
|
Returns:
|
|
732
725
|
True if .reviews/ has uncommitted changes
|
|
733
726
|
"""
|
|
734
|
-
reviews_dir = repo_root /
|
|
727
|
+
reviews_dir = repo_root / ".reviews"
|
|
735
728
|
if not reviews_dir.exists():
|
|
736
729
|
return False
|
|
737
730
|
|
|
738
|
-
result = _run_git(repo_root, [
|
|
731
|
+
result = _run_git(repo_root, ["status", "--porcelain", ".reviews/"])
|
|
739
732
|
return bool(result.stdout.strip())
|
|
740
733
|
|
|
741
734
|
|
|
@@ -752,19 +745,19 @@ def has_conflicts(repo_root: Path) -> bool:
|
|
|
752
745
|
True if there are unresolved merge conflicts
|
|
753
746
|
"""
|
|
754
747
|
# Check for merge in progress
|
|
755
|
-
git_dir = repo_root /
|
|
756
|
-
if (git_dir /
|
|
748
|
+
git_dir = repo_root / ".git"
|
|
749
|
+
if (git_dir / "MERGE_HEAD").exists():
|
|
757
750
|
# Merge in progress, check for conflict markers
|
|
758
|
-
result = _run_git(repo_root, [
|
|
751
|
+
result = _run_git(repo_root, ["diff", "--check"])
|
|
759
752
|
return result.returncode != 0
|
|
760
753
|
|
|
761
754
|
# Check for conflict markers in staged files
|
|
762
|
-
result = _run_git(repo_root, [
|
|
755
|
+
result = _run_git(repo_root, ["diff", "--cached", "--check"])
|
|
763
756
|
if result.returncode != 0:
|
|
764
757
|
return True
|
|
765
758
|
|
|
766
759
|
# Also check working tree
|
|
767
|
-
result = _run_git(repo_root, [
|
|
760
|
+
result = _run_git(repo_root, ["diff", "--check"])
|
|
768
761
|
return result.returncode != 0
|
|
769
762
|
|
|
770
763
|
|
|
@@ -772,7 +765,8 @@ def has_conflicts(repo_root: Path) -> bool:
|
|
|
772
765
|
# Commit and Push Operations (REQ-tv-d00013-G)
|
|
773
766
|
# =============================================================================
|
|
774
767
|
|
|
775
|
-
|
|
768
|
+
|
|
769
|
+
def commit_reviews(repo_root: Path, message: str, user: str = "system") -> bool:
|
|
776
770
|
"""
|
|
777
771
|
Commit changes to .reviews/ directory.
|
|
778
772
|
|
|
@@ -789,21 +783,18 @@ def commit_reviews(repo_root: Path, message: str, user: str = 'system') -> bool:
|
|
|
789
783
|
return True # No changes, success
|
|
790
784
|
|
|
791
785
|
# Stage .reviews/ changes
|
|
792
|
-
result = _run_git(repo_root, [
|
|
786
|
+
result = _run_git(repo_root, ["add", ".reviews/"])
|
|
793
787
|
if result.returncode != 0:
|
|
794
788
|
return False
|
|
795
789
|
|
|
796
790
|
# Commit with message
|
|
797
791
|
full_message = f"[review] {message}\n\nBy: {user}"
|
|
798
|
-
result = _run_git(repo_root, [
|
|
792
|
+
result = _run_git(repo_root, ["commit", "-m", full_message])
|
|
799
793
|
return result.returncode == 0
|
|
800
794
|
|
|
801
795
|
|
|
802
796
|
def commit_and_push_reviews(
|
|
803
|
-
repo_root: Path,
|
|
804
|
-
message: str,
|
|
805
|
-
user: str = 'system',
|
|
806
|
-
remote: str = 'origin'
|
|
797
|
+
repo_root: Path, message: str, user: str = "system", remote: str = "origin"
|
|
807
798
|
) -> Tuple[bool, str]:
|
|
808
799
|
"""
|
|
809
800
|
Commit changes to .reviews/ and push to remote.
|
|
@@ -822,42 +813,42 @@ def commit_and_push_reviews(
|
|
|
822
813
|
"""
|
|
823
814
|
# Check if there are changes
|
|
824
815
|
if not has_reviews_changes(repo_root):
|
|
825
|
-
return (True,
|
|
816
|
+
return (True, "No changes to commit")
|
|
826
817
|
|
|
827
818
|
# Stage .reviews/ changes
|
|
828
|
-
result = _run_git(repo_root, [
|
|
819
|
+
result = _run_git(repo_root, ["add", ".reviews/"])
|
|
829
820
|
if result.returncode != 0:
|
|
830
|
-
return (False, f
|
|
821
|
+
return (False, f"Failed to stage changes: {result.stderr}")
|
|
831
822
|
|
|
832
823
|
# Commit with message
|
|
833
824
|
full_message = f"[review] {message}\n\nBy: {user}"
|
|
834
|
-
result = _run_git(repo_root, [
|
|
825
|
+
result = _run_git(repo_root, ["commit", "-m", full_message])
|
|
835
826
|
if result.returncode != 0:
|
|
836
|
-
return (False, f
|
|
827
|
+
return (False, f"Failed to commit: {result.stderr}")
|
|
837
828
|
|
|
838
829
|
# Check if remote exists
|
|
839
830
|
if get_remote_name(repo_root) is None:
|
|
840
|
-
return (True,
|
|
831
|
+
return (True, "Committed locally (no remote configured)")
|
|
841
832
|
|
|
842
833
|
# Push to remote
|
|
843
834
|
current_branch = get_current_branch(repo_root)
|
|
844
835
|
if current_branch:
|
|
845
|
-
push_result = _run_git(repo_root, [
|
|
836
|
+
push_result = _run_git(repo_root, ["push", remote, current_branch])
|
|
846
837
|
if push_result.returncode == 0:
|
|
847
|
-
return (True,
|
|
838
|
+
return (True, "Committed and pushed successfully")
|
|
848
839
|
else:
|
|
849
840
|
# Commit succeeded but push failed - still return success for commit
|
|
850
|
-
return (True, f
|
|
841
|
+
return (True, f"Committed locally (push failed: {push_result.stderr})")
|
|
851
842
|
|
|
852
|
-
return (True,
|
|
843
|
+
return (True, "Committed locally")
|
|
853
844
|
|
|
854
845
|
|
|
855
846
|
# =============================================================================
|
|
856
847
|
# Fetch Operations (REQ-tv-d00013-I)
|
|
857
848
|
# =============================================================================
|
|
858
849
|
|
|
859
|
-
|
|
860
|
-
|
|
850
|
+
|
|
851
|
+
def fetch_package_branches(repo_root: Path, package_id: str, remote: str = "origin") -> List[str]:
|
|
861
852
|
"""
|
|
862
853
|
Fetch all remote branches for a package.
|
|
863
854
|
|
|
@@ -877,17 +868,23 @@ def fetch_package_branches(repo_root: Path, package_id: str,
|
|
|
877
868
|
return []
|
|
878
869
|
|
|
879
870
|
sanitized_package = _sanitize_branch_name(package_id)
|
|
880
|
-
refspec =
|
|
871
|
+
refspec = (
|
|
872
|
+
f"refs/heads/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*:"
|
|
873
|
+
f"refs/remotes/{remote}/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*"
|
|
874
|
+
)
|
|
881
875
|
|
|
882
876
|
# Fetch the specific package branches
|
|
883
|
-
|
|
877
|
+
_run_git(repo_root, ["fetch", remote, refspec])
|
|
884
878
|
|
|
885
879
|
# Even if fetch fails (e.g., no matching refs), list what we have
|
|
886
880
|
branches = []
|
|
887
|
-
list_result = _run_git(
|
|
881
|
+
list_result = _run_git(
|
|
882
|
+
repo_root,
|
|
883
|
+
["branch", "-r", "--list", f"{remote}/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*"],
|
|
884
|
+
)
|
|
888
885
|
|
|
889
886
|
if list_result.returncode == 0:
|
|
890
|
-
for line in list_result.stdout.strip().split(
|
|
887
|
+
for line in list_result.stdout.strip().split("\n"):
|
|
891
888
|
branch = line.strip()
|
|
892
889
|
if branch:
|
|
893
890
|
branches.append(branch)
|
|
@@ -895,7 +892,7 @@ def fetch_package_branches(repo_root: Path, package_id: str,
|
|
|
895
892
|
return branches
|
|
896
893
|
|
|
897
894
|
|
|
898
|
-
def fetch_review_branches(repo_root: Path, remote: str =
|
|
895
|
+
def fetch_review_branches(repo_root: Path, remote: str = "origin") -> bool:
|
|
899
896
|
"""
|
|
900
897
|
Fetch all review branches from remote.
|
|
901
898
|
|
|
@@ -909,7 +906,7 @@ def fetch_review_branches(repo_root: Path, remote: str = 'origin') -> bool:
|
|
|
909
906
|
if get_remote_name(repo_root) is None:
|
|
910
907
|
return False
|
|
911
908
|
|
|
912
|
-
result = _run_git(repo_root, [
|
|
909
|
+
result = _run_git(repo_root, ["fetch", remote, "--prune"])
|
|
913
910
|
return result.returncode == 0
|
|
914
911
|
|
|
915
912
|
|
|
@@ -917,6 +914,7 @@ def fetch_review_branches(repo_root: Path, remote: str = 'origin') -> bool:
|
|
|
917
914
|
# Branch Listing and Cleanup (REQ-tv-d00013 CLI Service Layer)
|
|
918
915
|
# =============================================================================
|
|
919
916
|
|
|
917
|
+
|
|
920
918
|
@dataclass
|
|
921
919
|
class CleanupResult:
|
|
922
920
|
"""
|
|
@@ -924,18 +922,17 @@ class CleanupResult:
|
|
|
924
922
|
|
|
925
923
|
REQ-tv-d00013: Cleanup result for CLI feedback.
|
|
926
924
|
"""
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
925
|
+
|
|
926
|
+
deleted_local: List[str] # Branches deleted locally
|
|
927
|
+
deleted_remote: List[str] # Branches deleted from remote
|
|
928
|
+
skipped_current: List[str] # Skipped because current branch
|
|
929
|
+
skipped_unpushed: List[str] # Skipped because has unpushed commits
|
|
930
|
+
skipped_unmerged: List[str] # Skipped because not merged
|
|
931
|
+
errors: List[Tuple[str, str]] # (branch, error_message) pairs
|
|
933
932
|
|
|
934
933
|
|
|
935
934
|
def list_review_branches_with_info(
|
|
936
|
-
repo_root: Path,
|
|
937
|
-
package_id: Optional[str] = None,
|
|
938
|
-
user: Optional[str] = None
|
|
935
|
+
repo_root: Path, package_id: Optional[str] = None, user: Optional[str] = None
|
|
939
936
|
) -> List[BranchInfo]:
|
|
940
937
|
"""
|
|
941
938
|
List all review branches with their metadata.
|
|
@@ -988,7 +985,7 @@ def delete_review_branch(
|
|
|
988
985
|
branch_name: str,
|
|
989
986
|
delete_remote: bool = False,
|
|
990
987
|
force: bool = False,
|
|
991
|
-
remote: str =
|
|
988
|
+
remote: str = "origin",
|
|
992
989
|
) -> Tuple[bool, str]:
|
|
993
990
|
"""
|
|
994
991
|
Delete a review branch with safety checks.
|
|
@@ -1037,14 +1034,14 @@ def delete_review_branch(
|
|
|
1037
1034
|
return (False, f"Branch is not merged into main: {branch_name}")
|
|
1038
1035
|
|
|
1039
1036
|
# Delete local branch
|
|
1040
|
-
delete_flag =
|
|
1041
|
-
result = _run_git(repo_root, [
|
|
1037
|
+
delete_flag = "-D" if force else "-d"
|
|
1038
|
+
result = _run_git(repo_root, ["branch", delete_flag, branch_name])
|
|
1042
1039
|
if result.returncode != 0:
|
|
1043
1040
|
return (False, f"Failed to delete local branch: {result.stderr}")
|
|
1044
1041
|
|
|
1045
1042
|
# Delete remote branch if requested
|
|
1046
1043
|
if delete_remote and remote_branch_exists(repo_root, branch_name, remote):
|
|
1047
|
-
result = _run_git(repo_root, [
|
|
1044
|
+
result = _run_git(repo_root, ["push", remote, "--delete", branch_name])
|
|
1048
1045
|
if result.returncode != 0:
|
|
1049
1046
|
return (True, f"Deleted local branch, but failed to delete remote: {result.stderr}")
|
|
1050
1047
|
return (True, f"Deleted local and remote branch: {branch_name}")
|
|
@@ -1059,7 +1056,7 @@ def cleanup_review_branches(
|
|
|
1059
1056
|
only_merged: bool = True,
|
|
1060
1057
|
delete_remote: bool = False,
|
|
1061
1058
|
dry_run: bool = False,
|
|
1062
|
-
remote: str =
|
|
1059
|
+
remote: str = "origin",
|
|
1063
1060
|
) -> CleanupResult:
|
|
1064
1061
|
"""
|
|
1065
1062
|
Delete review branches matching criteria.
|
|
@@ -1092,7 +1089,7 @@ def cleanup_review_branches(
|
|
|
1092
1089
|
skipped_current=[],
|
|
1093
1090
|
skipped_unpushed=[],
|
|
1094
1091
|
skipped_unmerged=[],
|
|
1095
|
-
errors=[]
|
|
1092
|
+
errors=[],
|
|
1096
1093
|
)
|
|
1097
1094
|
|
|
1098
1095
|
# Get branches to consider
|
|
@@ -1126,15 +1123,13 @@ def cleanup_review_branches(
|
|
|
1126
1123
|
result.deleted_remote.append(branch.name)
|
|
1127
1124
|
else:
|
|
1128
1125
|
# Delete local branch
|
|
1129
|
-
delete_result = _run_git(repo_root, [
|
|
1126
|
+
delete_result = _run_git(repo_root, ["branch", "-d", branch.name])
|
|
1130
1127
|
if delete_result.returncode == 0:
|
|
1131
1128
|
result.deleted_local.append(branch.name)
|
|
1132
1129
|
|
|
1133
1130
|
# Delete remote if requested
|
|
1134
1131
|
if delete_remote and branch.has_remote:
|
|
1135
|
-
remote_result = _run_git(
|
|
1136
|
-
repo_root, ['push', remote, '--delete', branch.name]
|
|
1137
|
-
)
|
|
1132
|
+
remote_result = _run_git(repo_root, ["push", remote, "--delete", branch.name])
|
|
1138
1133
|
if remote_result.returncode == 0:
|
|
1139
1134
|
result.deleted_remote.append(branch.name)
|
|
1140
1135
|
else:
|
|
@@ -1142,8 +1137,6 @@ def cleanup_review_branches(
|
|
|
1142
1137
|
(branch.name, f"Failed to delete remote: {remote_result.stderr}")
|
|
1143
1138
|
)
|
|
1144
1139
|
else:
|
|
1145
|
-
result.errors.append(
|
|
1146
|
-
(branch.name, f"Failed to delete: {delete_result.stderr}")
|
|
1147
|
-
)
|
|
1140
|
+
result.errors.append((branch.name, f"Failed to delete: {delete_result.stderr}"))
|
|
1148
1141
|
|
|
1149
1142
|
return result
|