-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcore.py
165 lines (133 loc) · 5.4 KB
/
core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env python
"""
core.py — LazyTag Core Logic
Provides the main functionality for tagging modified lines in staged files.
Used by both the CLI (lazytag.py) and Git hook integration.
"""
import subprocess
import re
from pathlib import Path
COMMENT_CHARS = {
'.py': '#',
'.c': '//',
'.cpp': '//',
'.h': '//',
'.hpp': '//',
'.rs': '//',
'.adb': '--',
'.ads': '--',
'.ada': '--',
}
MAX_LINE_LENGTH = 80
def get_branch_tag():
try:
branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).decode().strip()
match = re.search(r'([A-Z]+-\d+)', branch)
return match.group(1) if match else None
except subprocess.CalledProcessError:
return None
def get_staged_files():
output = subprocess.check_output(['git', 'diff', '--cached', '--name-only']).decode()
return [line.strip() for line in output.splitlines() if Path(line.strip()).suffix.lower() in COMMENT_CHARS]
def get_file_diff_lines(filename):
output = subprocess.check_output(['git', 'diff', '--cached', '-U0', filename]).decode()
changes = []
for line in output.splitlines():
if line.startswith('@@'):
m = re.search(r'\+(\d+)(?:,(\d+))?', line)
if m:
start = int(m.group(1))
count = int(m.group(2) or 1)
for i in range(start, start + count):
changes.append(i)
return set(changes)
def extract_tags(comment):
return [t.strip() for t in comment.split(',') if re.match(r'[A-Z]+-\d+', t)]
def should_tag_comment_line(line, ext):
comment_char = COMMENT_CHARS.get(ext, '').lower()
stripped = line.strip().lower()
return stripped.startswith(f"{comment_char}deleted") or stripped.startswith(f"{comment_char} deleted")
def is_code_line(line, ext):
stripped = line.strip()
return bool(stripped) and not stripped.startswith(COMMENT_CHARS[ext])
def align_tags_with_comments(line, tags, comment_char, new_tag):
if new_tag not in tags:
tags.append(new_tag)
tag_block = f"{comment_char} {', '.join(tags)}"
line = line.rstrip()
if comment_char in line:
split_index = line.find(comment_char)
code_part = line[:split_index].rstrip()
comment_part = line[split_index:].rstrip()
else:
code_part = line
comment_part = ""
combined_comment = f"{comment_part} {tag_block}" if comment_part else tag_block
total_len = len(code_part) + 1 + len(combined_comment)
if total_len > MAX_LINE_LENGTH:
return f"{code_part} {combined_comment}"
else:
padding = MAX_LINE_LENGTH - len(code_part) - len(combined_comment)
return f"{code_part}{' ' * padding}{combined_comment}"
def align_tags_to_col_80_preserve_deleted(line, tags, comment_char, new_tag):
if new_tag not in tags:
tags.append(new_tag)
tag_block = f"{comment_char} {', '.join(tags)}"
line = line.rstrip()
tag_search_start = 40
tag_pos = line.find(comment_char, tag_search_start)
code_part = line[:tag_pos].rstrip() if tag_pos != -1 else line.rstrip()
total_len = len(code_part) + 1 + len(tag_block)
if total_len > MAX_LINE_LENGTH:
return f"{code_part} {tag_block}"
else:
padding = MAX_LINE_LENGTH - len(code_part) - len(tag_block)
return f"{code_part}{' ' * padding}{tag_block}"
def process_file(filepath, tag, dry_run=False):
ext = Path(filepath).suffix.lower()
comment_char = COMMENT_CHARS[ext]
changes = get_file_diff_lines(filepath)
with open(filepath, 'r') as f:
lines = f.readlines()
updated_lines = []
modified = False
for idx, line in enumerate(lines, start=1):
original = line.rstrip('\n')
modified_line = original
update_needed = False
if idx in changes:
if is_code_line(original, ext):
match = re.search(rf'{re.escape(comment_char)}\s*(.*)$', original)
tags = extract_tags(match.group(1)) if match else []
modified_line = align_tags_with_comments(original, tags, comment_char, tag)
update_needed = True
elif should_tag_comment_line(original, ext):
match = re.search(rf'{re.escape(comment_char)}\s*(.*)$', original)
tags = extract_tags(match.group(1)) if match else []
modified_line = align_tags_to_col_80_preserve_deleted(original, tags, comment_char, tag)
update_needed = True
if update_needed:
modified = True
log_type = "DRY-RUN" if dry_run else "TAGGED"
print(f"[{log_type}] {filepath}:{idx} --> {modified_line}")
updated_lines.append(modified_line + '\n')
if modified and not dry_run:
with open(filepath, 'w') as f:
f.writelines(updated_lines)
subprocess.run(['git', 'add', filepath])
def process_files_with_tag(preferred_tag=None, dry_run=False):
tag = preferred_tag or get_branch_tag()
if not tag:
print("[ERROR] No Jira-style tag found (use --tag or a branch like ABC-1234-feature)")
return
files = get_staged_files()
if not files:
print("[INFO] No staged source files to process.")
return
print(f"[INFO] Tagging with: {tag}\n")
for file in files:
process_file(file, tag, dry_run)
if dry_run:
print("\n[DRY-RUN] Complete. No files were modified.")
else:
print("\n[SUCCESS] Tagging complete.")