imp-git 0.0.26__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.
- imp/__init__.py +7 -0
- imp/__main__.py +3 -0
- imp/ai.py +130 -0
- imp/commands/__init__.py +0 -0
- imp/commands/amend.py +68 -0
- imp/commands/branch.py +72 -0
- imp/commands/clean.py +47 -0
- imp/commands/commit.py +67 -0
- imp/commands/config.py +62 -0
- imp/commands/doctor.py +87 -0
- imp/commands/done.py +98 -0
- imp/commands/fix.py +78 -0
- imp/commands/help.py +103 -0
- imp/commands/log.py +25 -0
- imp/commands/pr.py +125 -0
- imp/commands/push.py +42 -0
- imp/commands/release.py +273 -0
- imp/commands/resolve.py +161 -0
- imp/commands/revert.py +83 -0
- imp/commands/review.py +76 -0
- imp/commands/ship.py +131 -0
- imp/commands/split.py +176 -0
- imp/commands/status.py +96 -0
- imp/commands/sync.py +64 -0
- imp/commands/undo.py +45 -0
- imp/config.py +51 -0
- imp/console.py +168 -0
- imp/git.py +428 -0
- imp/main.py +74 -0
- imp/prompts.py +168 -0
- imp/theme.py +14 -0
- imp/validate.py +28 -0
- imp/version.py +67 -0
- imp_git-0.0.26.dist-info/METADATA +226 -0
- imp_git-0.0.26.dist-info/RECORD +38 -0
- imp_git-0.0.26.dist-info/WHEEL +4 -0
- imp_git-0.0.26.dist-info/entry_points.txt +2 -0
- imp_git-0.0.26.dist-info/licenses/LICENSE +21 -0
imp/git.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from imp import console
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _run (*args: str, check: bool = True, timeout: int = 60, env: dict [str, str] | None = None) -> subprocess.CompletedProcess [str]:
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
run_env = None
|
|
12
|
+
if env:
|
|
13
|
+
run_env = { **os.environ, **env }
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
return subprocess.run (
|
|
17
|
+
[ "git", *args ],
|
|
18
|
+
capture_output=True,
|
|
19
|
+
text=True,
|
|
20
|
+
check=check,
|
|
21
|
+
timeout=timeout,
|
|
22
|
+
env=run_env,
|
|
23
|
+
)
|
|
24
|
+
except subprocess.TimeoutExpired:
|
|
25
|
+
console.err (f"git {args [0]} timed out")
|
|
26
|
+
raise typer.Exit (1) from None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def require ():
|
|
30
|
+
result = _run ("rev-parse", "--git-dir", check=False)
|
|
31
|
+
if result.returncode != 0:
|
|
32
|
+
console.err ("Not a git repository")
|
|
33
|
+
raise typer.Exit (1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def require_clean (hint: str = "imp commit first"):
|
|
37
|
+
if not is_clean ():
|
|
38
|
+
console.err ("Uncommitted changes")
|
|
39
|
+
console.hint (hint)
|
|
40
|
+
raise typer.Exit (1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def stage (all: bool = False):
|
|
44
|
+
if all:
|
|
45
|
+
_run ("add", "-A")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def add (files: list [str]):
|
|
49
|
+
_run ("add", "--", *files)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def staged_files () -> list [str]:
|
|
53
|
+
result = _run ("diff", "--cached", "--name-only", check=False)
|
|
54
|
+
return [ f.strip () for f in result.stdout.splitlines () if f.strip () ]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def diff (staged: bool = False) -> str:
|
|
58
|
+
args = [ "diff" ]
|
|
59
|
+
if staged:
|
|
60
|
+
args.append ("--cached")
|
|
61
|
+
|
|
62
|
+
result = _run (*args)
|
|
63
|
+
return result.stdout
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def diff_range (rev_range: str, max_lines: int = 0) -> str:
|
|
67
|
+
args = [ "diff", rev_range ]
|
|
68
|
+
result = _run (*args, check=False)
|
|
69
|
+
text = result.stdout
|
|
70
|
+
if max_lines > 0:
|
|
71
|
+
lines = text.splitlines ()
|
|
72
|
+
text = "\n".join (lines [:max_lines])
|
|
73
|
+
return text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def diff_names () -> list [str]:
|
|
77
|
+
staged = _run ("diff", "--cached", "--name-only", check=False).stdout.strip ()
|
|
78
|
+
unstaged = _run ("diff", "--name-only", check=False).stdout.strip ()
|
|
79
|
+
untracked = _run (
|
|
80
|
+
"ls-files", "--others", "--exclude-standard",
|
|
81
|
+
check=False,
|
|
82
|
+
).stdout.strip ()
|
|
83
|
+
|
|
84
|
+
names = set ()
|
|
85
|
+
for block in [ staged, unstaged, untracked ]:
|
|
86
|
+
for line in block.splitlines ():
|
|
87
|
+
line = line.strip ()
|
|
88
|
+
if line:
|
|
89
|
+
names.add (line)
|
|
90
|
+
|
|
91
|
+
return sorted (names)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def diff_file (path: str) -> str:
|
|
95
|
+
result = _run ("diff", "HEAD", "--", path, check=False)
|
|
96
|
+
text = result.stdout
|
|
97
|
+
if not text:
|
|
98
|
+
result = _run ("diff", "--cached", "--", path, check=False)
|
|
99
|
+
text = result.stdout
|
|
100
|
+
return text
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def diff_numstat () -> str:
|
|
104
|
+
staged = _run ("diff", "--cached", "--numstat", check=False).stdout.strip ()
|
|
105
|
+
unstaged = _run ("diff", "--numstat", check=False).stdout.strip ()
|
|
106
|
+
combined = ""
|
|
107
|
+
if staged:
|
|
108
|
+
combined += staged + "\n"
|
|
109
|
+
if unstaged:
|
|
110
|
+
combined += unstaged
|
|
111
|
+
return combined.strip ()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def branch () -> str:
|
|
115
|
+
result = _run ("branch", "--show-current", check=False)
|
|
116
|
+
return result.stdout.strip ()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def branches_local () -> list [str]:
|
|
120
|
+
result = _run ("branch", "--format=%(refname:short)", check=False)
|
|
121
|
+
return [ b.strip () for b in result.stdout.splitlines () if b.strip () ]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def branches_merged (base: str) -> list [str]:
|
|
125
|
+
result = _run ("branch", "--merged", base, check=False)
|
|
126
|
+
current = branch ()
|
|
127
|
+
merged = []
|
|
128
|
+
for line in result.stdout.splitlines ():
|
|
129
|
+
name = line.removeprefix ("* ").strip ()
|
|
130
|
+
if name and name != base and name != current:
|
|
131
|
+
merged.append (name)
|
|
132
|
+
return merged
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def commit (msg: str, amend: bool = False, date: str = ""):
|
|
136
|
+
args = [ "commit", "-m", msg ]
|
|
137
|
+
if amend:
|
|
138
|
+
args.insert (1, "--amend")
|
|
139
|
+
if date:
|
|
140
|
+
args.extend ([ "--date", date ])
|
|
141
|
+
|
|
142
|
+
_run (*args, env={ "GIT_COMMITTER_DATE": date } if date else {})
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def commit_count () -> int:
|
|
146
|
+
result = _run ("rev-list", "--count", "HEAD", check=False)
|
|
147
|
+
try:
|
|
148
|
+
return int (result.stdout.strip ())
|
|
149
|
+
except ValueError:
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def is_clean () -> bool:
|
|
154
|
+
result = _run ("status", "--porcelain")
|
|
155
|
+
return result.stdout.strip () == ""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def base_branch () -> str:
|
|
159
|
+
for name in [ "main", "master" ]:
|
|
160
|
+
result = _run ("rev-parse", "--verify", name, check=False)
|
|
161
|
+
if result.returncode == 0:
|
|
162
|
+
return name
|
|
163
|
+
|
|
164
|
+
return "main"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def last_tag () -> str:
|
|
168
|
+
result = _run ("describe", "--tags", "--abbrev=0", check=False)
|
|
169
|
+
return result.stdout.strip ()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def highest_tag () -> str:
|
|
173
|
+
result = _run ("tag", "-l", "v*", "--sort=-v:refname", check=False)
|
|
174
|
+
lines = result.stdout.strip ().splitlines ()
|
|
175
|
+
return lines [0].strip () if lines and lines [0].strip () else ""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def tag (name: str):
|
|
179
|
+
_run ("tag", name)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def tag_exists (name: str) -> bool:
|
|
183
|
+
result = _run ("rev-parse", name, check=False)
|
|
184
|
+
return result.returncode == 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def tag_delete (name: str):
|
|
188
|
+
_run ("tag", "-d", name, check=False)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def has_upstream () -> bool:
|
|
192
|
+
result = _run ("rev-parse", "--verify", "@{u}", check=False)
|
|
193
|
+
return result.returncode == 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def count_ahead () -> int:
|
|
197
|
+
result = _run ("rev-list", "--count", "@{u}..HEAD", check=False)
|
|
198
|
+
try:
|
|
199
|
+
return int (result.stdout.strip ())
|
|
200
|
+
except ValueError:
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def count_behind () -> int:
|
|
205
|
+
result = _run ("rev-list", "--count", "HEAD..@{u}", check=False)
|
|
206
|
+
try:
|
|
207
|
+
return int (result.stdout.strip ())
|
|
208
|
+
except ValueError:
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def log_oneline (count: int = 10, rev_range: str = "") -> str:
|
|
213
|
+
args = [ "log", "--oneline" ]
|
|
214
|
+
if rev_range:
|
|
215
|
+
args.append (rev_range)
|
|
216
|
+
else:
|
|
217
|
+
args.extend ([ "-n", str (count) ])
|
|
218
|
+
result = _run (*args, check=False)
|
|
219
|
+
return result.stdout.strip ()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def log_graph (count: int = 20, ref: str = "") -> str:
|
|
223
|
+
args = [
|
|
224
|
+
"log", "--oneline", "--graph",
|
|
225
|
+
"--decorate", "--color=always",
|
|
226
|
+
"-n", str (count),
|
|
227
|
+
]
|
|
228
|
+
if ref:
|
|
229
|
+
args.append (ref)
|
|
230
|
+
result = _run (*args, check=False)
|
|
231
|
+
return result.stdout.strip ()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def log_subjects (rev_range: str = "", count: int = 0) -> str:
|
|
235
|
+
args = [ "log", "--format=%s" ]
|
|
236
|
+
if rev_range:
|
|
237
|
+
args.append (rev_range)
|
|
238
|
+
elif count > 0:
|
|
239
|
+
args.extend ([ "-n", str (count) ])
|
|
240
|
+
result = _run (*args, check=False)
|
|
241
|
+
return result.stdout.strip ()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def fetch (prune: bool = False):
|
|
245
|
+
args = [ "fetch" ]
|
|
246
|
+
if prune:
|
|
247
|
+
args.append ("--prune")
|
|
248
|
+
_run (*args, check=False)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def rebase () -> bool:
|
|
252
|
+
result = _run ("rebase", check=False)
|
|
253
|
+
return result.returncode == 0
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def push (
|
|
257
|
+
force_lease: bool = False,
|
|
258
|
+
set_upstream: bool = False,
|
|
259
|
+
target: str = "",
|
|
260
|
+
ref: str = "",
|
|
261
|
+
):
|
|
262
|
+
args = [ "push" ]
|
|
263
|
+
if force_lease:
|
|
264
|
+
args.append ("--force-with-lease")
|
|
265
|
+
if set_upstream:
|
|
266
|
+
args.extend ([ "-u", "origin" ])
|
|
267
|
+
if target:
|
|
268
|
+
args.append (target)
|
|
269
|
+
elif ref:
|
|
270
|
+
args.extend ([ "origin", ref ])
|
|
271
|
+
_run (*args)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def merge (ref: str, no_ff: bool = False) -> bool:
|
|
275
|
+
args = [ "merge" ]
|
|
276
|
+
if no_ff:
|
|
277
|
+
args.append ("--no-ff")
|
|
278
|
+
args.append (ref)
|
|
279
|
+
result = _run (*args, check=False)
|
|
280
|
+
return result.returncode == 0
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def is_merged (branch_name: str, into: str) -> bool:
|
|
284
|
+
result = _run ("merge-base", "--is-ancestor", branch_name, into, check=False)
|
|
285
|
+
return result.returncode == 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def pull ():
|
|
289
|
+
_run ("pull", check=False)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def revert_commit (ref: str, no_commit: bool = False):
|
|
293
|
+
args = [ "revert" ]
|
|
294
|
+
if no_commit:
|
|
295
|
+
args.append ("--no-commit")
|
|
296
|
+
args.append (ref)
|
|
297
|
+
_run (*args)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def revert_abort ():
|
|
301
|
+
_run ("revert", "--abort", check=False)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def reset (ref: str, soft: bool = False, hard: bool = False):
|
|
305
|
+
args = [ "reset" ]
|
|
306
|
+
if soft:
|
|
307
|
+
args.append ("--soft")
|
|
308
|
+
elif hard:
|
|
309
|
+
args.append ("--hard")
|
|
310
|
+
args.append (ref)
|
|
311
|
+
_run (*args)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def checkout (ref: str, create: bool = False):
|
|
315
|
+
args = [ "checkout" ]
|
|
316
|
+
if create:
|
|
317
|
+
args.append ("-b")
|
|
318
|
+
args.append (ref)
|
|
319
|
+
_run (*args)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def show (ref: str = "HEAD", fmt: str = "", stat: bool = False) -> str:
|
|
323
|
+
args = [ "show" ]
|
|
324
|
+
if fmt:
|
|
325
|
+
args.append (f"--format={fmt}")
|
|
326
|
+
else:
|
|
327
|
+
args.append ("--format=")
|
|
328
|
+
if stat:
|
|
329
|
+
args.append ("--stat")
|
|
330
|
+
args.append (ref)
|
|
331
|
+
result = _run (*args, check=False)
|
|
332
|
+
return result.stdout.strip ()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def status_short () -> str:
|
|
336
|
+
result = _run ("status", "--short")
|
|
337
|
+
return result.stdout.strip ()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def worktree_list () -> str:
|
|
341
|
+
result = _run ("worktree", "list", check=False)
|
|
342
|
+
return result.stdout.strip ()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def remote_has_branch (name: str) -> bool:
|
|
346
|
+
result = _run ("ls-remote", "--heads", "origin", name, check=False)
|
|
347
|
+
return name in result.stdout
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def remote_exists () -> bool:
|
|
351
|
+
result = _run ("remote", check=False)
|
|
352
|
+
return result.stdout.strip () != ""
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def delete_branch (name: str, force: bool = False, remote: bool = False) -> bool:
|
|
356
|
+
if remote:
|
|
357
|
+
result = _run ("push", "origin", "--delete", name, check=False)
|
|
358
|
+
return result.returncode == 0
|
|
359
|
+
else:
|
|
360
|
+
flag = "-D" if force else "-d"
|
|
361
|
+
result = _run ("branch", flag, name, check=False)
|
|
362
|
+
return result.returncode == 0
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def unstage (files: list [str] | None = None):
|
|
366
|
+
if files:
|
|
367
|
+
if commit_count () > 0:
|
|
368
|
+
_run ("reset", "HEAD", "--", *files, check=False)
|
|
369
|
+
else:
|
|
370
|
+
_run ("rm", "--cached", "--", *files, check=False)
|
|
371
|
+
elif commit_count () > 0:
|
|
372
|
+
_run ("reset", "HEAD", check=False)
|
|
373
|
+
else:
|
|
374
|
+
_run ("rm", "-r", "--cached", ".", check=False)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def repo_root () -> str:
|
|
378
|
+
result = _run ("rev-parse", "--show-toplevel", check=False)
|
|
379
|
+
return result.stdout.strip ()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def repo_name () -> str:
|
|
383
|
+
from pathlib import Path
|
|
384
|
+
return Path (repo_root ()).name
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def rev_parse (ref: str) -> str:
|
|
388
|
+
result = _run ("rev-parse", ref, check=False)
|
|
389
|
+
return result.stdout.strip ()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def rev_parse_short (ref: str) -> str:
|
|
393
|
+
result = _run ("rev-parse", "--short", ref, check=False)
|
|
394
|
+
return result.stdout.strip ()
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def conflicts () -> list [str]:
|
|
398
|
+
result = _run ("diff", "--name-only", "--diff-filter=U", check=False)
|
|
399
|
+
lines = result.stdout.strip ().splitlines ()
|
|
400
|
+
return [ line.strip () for line in lines if line.strip () ]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def conflict_content (path: str) -> str:
|
|
404
|
+
from pathlib import Path
|
|
405
|
+
|
|
406
|
+
return Path (path).read_text ()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def merge_in_progress () -> bool:
|
|
410
|
+
from pathlib import Path
|
|
411
|
+
|
|
412
|
+
git_dir = _run ("rev-parse", "--git-dir", check=False).stdout.strip ()
|
|
413
|
+
return Path (git_dir, "MERGE_HEAD").exists ()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def rebase_in_progress () -> bool:
|
|
417
|
+
from pathlib import Path
|
|
418
|
+
|
|
419
|
+
git_dir = _run ("rev-parse", "--git-dir", check=False).stdout.strip ()
|
|
420
|
+
return (
|
|
421
|
+
Path (git_dir, "rebase-merge").exists ()
|
|
422
|
+
or Path (git_dir, "rebase-apply").exists ()
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def branch_age (name: str) -> str:
|
|
427
|
+
result = _run ("log", "-1", "--format=%cr", name, check=False)
|
|
428
|
+
return result.stdout.strip () or "unknown"
|
imp/main.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from imp import __version__
|
|
5
|
+
from imp.commands.amend import amend
|
|
6
|
+
from imp.commands.branch import branch
|
|
7
|
+
from imp.commands.clean import clean
|
|
8
|
+
from imp.commands.commit import commit
|
|
9
|
+
from imp.commands.config import configure
|
|
10
|
+
from imp.commands.doctor import doctor
|
|
11
|
+
from imp.commands.done import done
|
|
12
|
+
from imp.commands.fix import fix
|
|
13
|
+
from imp.commands.help import help
|
|
14
|
+
from imp.commands.log import log
|
|
15
|
+
from imp.commands.pr import pr
|
|
16
|
+
from imp.commands.push import push
|
|
17
|
+
from imp.commands.release import release
|
|
18
|
+
from imp.commands.resolve import resolve
|
|
19
|
+
from imp.commands.revert import revert
|
|
20
|
+
from imp.commands.review import review
|
|
21
|
+
from imp.commands.ship import ship
|
|
22
|
+
from imp.commands.split import split
|
|
23
|
+
from imp.commands.status import status
|
|
24
|
+
from imp.commands.sync import sync
|
|
25
|
+
from imp.commands.undo import undo
|
|
26
|
+
|
|
27
|
+
app = typer.Typer (
|
|
28
|
+
name="imp",
|
|
29
|
+
no_args_is_help=True,
|
|
30
|
+
rich_markup_mode="rich",
|
|
31
|
+
add_completion=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _version (value: bool):
|
|
36
|
+
if value:
|
|
37
|
+
Console ().print (f"imp {__version__}")
|
|
38
|
+
raise typer.Exit ()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.callback ()
|
|
42
|
+
def main (
|
|
43
|
+
version: bool | None = typer.Option (
|
|
44
|
+
None,
|
|
45
|
+
"--version", "-v",
|
|
46
|
+
help="Show version and exit",
|
|
47
|
+
callback=_version,
|
|
48
|
+
is_eager=True,
|
|
49
|
+
),
|
|
50
|
+
):
|
|
51
|
+
"""[green]imp[/green] — AI-powered git workflow"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
app.command () (amend)
|
|
55
|
+
app.command () (branch)
|
|
56
|
+
app.command () (clean)
|
|
57
|
+
app.command () (commit)
|
|
58
|
+
app.command ("config") (configure)
|
|
59
|
+
app.command () (doctor)
|
|
60
|
+
app.command () (done)
|
|
61
|
+
app.command () (fix)
|
|
62
|
+
app.command () (help)
|
|
63
|
+
app.command () (log)
|
|
64
|
+
app.command () (pr)
|
|
65
|
+
app.command () (push)
|
|
66
|
+
app.command () (release)
|
|
67
|
+
app.command () (resolve)
|
|
68
|
+
app.command () (revert)
|
|
69
|
+
app.command () (review)
|
|
70
|
+
app.command () (ship)
|
|
71
|
+
app.command () (split)
|
|
72
|
+
app.command () (status)
|
|
73
|
+
app.command () (sync)
|
|
74
|
+
app.command () (undo)
|
imp/prompts.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _ticket_rule (branch: str) -> str:
|
|
5
|
+
match = re.search (r"([A-Z]+-[0-9]+)", branch)
|
|
6
|
+
if not match:
|
|
7
|
+
return ""
|
|
8
|
+
|
|
9
|
+
ticket = match.group (1)
|
|
10
|
+
return f'- Include ticket {ticket} after the type, e.g. "fix: {ticket} message"\n'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _whisper (text: str) -> str:
|
|
14
|
+
if not text:
|
|
15
|
+
return ""
|
|
16
|
+
return f"\nUser hint: {text}\n"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def commit (diff: str, branch: str = "", whisper: str = "") -> str:
|
|
20
|
+
return f"""\
|
|
21
|
+
Generate a Conventional Commits message for this diff.
|
|
22
|
+
{_whisper (whisper)}\
|
|
23
|
+
Format: type: message
|
|
24
|
+
Types: feat, fix, refactor, build, chore, docs, test, style, perf, ci
|
|
25
|
+
{_ticket_rule (branch)}
|
|
26
|
+
Rules:
|
|
27
|
+
- Subject only, one line, max 72 chars, no period
|
|
28
|
+
- ALL LOWERCASE after the colon (except ticket IDs like IMP-123)
|
|
29
|
+
- Imperative mood: "add" not "added", "fix" not "fixes"
|
|
30
|
+
- Pick the type that best fits the primary change
|
|
31
|
+
- No markdown, no backticks, no quotes
|
|
32
|
+
- No body, no bullet points, just the subject line
|
|
33
|
+
- Output will be validated against commitlint rules; it must pass
|
|
34
|
+
|
|
35
|
+
Diff:
|
|
36
|
+
{diff}
|
|
37
|
+
|
|
38
|
+
Output ONLY the commit message, nothing else:"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def review (diff: str, whisper: str = "") -> str:
|
|
42
|
+
return f"""\
|
|
43
|
+
Review this code diff. Be concise and actionable.
|
|
44
|
+
{_whisper (whisper)}\
|
|
45
|
+
Check for:
|
|
46
|
+
- Bugs or logic errors
|
|
47
|
+
- Security issues
|
|
48
|
+
- Performance problems
|
|
49
|
+
- Code style issues
|
|
50
|
+
- Missing error handling
|
|
51
|
+
|
|
52
|
+
If the code looks good, say so briefly.
|
|
53
|
+
|
|
54
|
+
Diff:
|
|
55
|
+
{diff}
|
|
56
|
+
|
|
57
|
+
Output ONLY the review:"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def branch_name (description: str, whisper: str = "") -> str:
|
|
61
|
+
return f"""\
|
|
62
|
+
Suggest a git branch name for: {description}
|
|
63
|
+
{_whisper (whisper)}\
|
|
64
|
+
Rules:
|
|
65
|
+
- Lowercase, hyphens only, no spaces
|
|
66
|
+
- Max 30 chars
|
|
67
|
+
- Format: type/short-name
|
|
68
|
+
- Types: feat, fix, refactor, docs, test, chore
|
|
69
|
+
|
|
70
|
+
Output ONLY the branch name:"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def revert (commit_msg: str, diff: str, whisper: str = "") -> str:
|
|
74
|
+
return f"""\
|
|
75
|
+
Generate a commit message for reverting this change. Start with 'Revert:'. Max 50 chars:
|
|
76
|
+
{_whisper (whisper)}\
|
|
77
|
+
Original: {commit_msg}
|
|
78
|
+
|
|
79
|
+
Changes reverted:
|
|
80
|
+
{diff}
|
|
81
|
+
|
|
82
|
+
Output ONLY the commit message:"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def fix (title: str, body: str, whisper: str = "") -> str:
|
|
86
|
+
return f"""\
|
|
87
|
+
Suggest a git branch name for fixing this issue:
|
|
88
|
+
{_whisper (whisper)}\
|
|
89
|
+
Title: {title}
|
|
90
|
+
Description: {body}
|
|
91
|
+
|
|
92
|
+
Rules:
|
|
93
|
+
- Lowercase, hyphens only
|
|
94
|
+
- Max 30 chars
|
|
95
|
+
- Format: fix/<short-name>
|
|
96
|
+
- Include issue number if fits
|
|
97
|
+
|
|
98
|
+
Output ONLY the branch name:"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def pr (branch: str, log: str, diff: str, whisper: str = "") -> str:
|
|
102
|
+
return f"""\
|
|
103
|
+
Generate a GitHub pull request title and description.
|
|
104
|
+
{_whisper (whisper)}\
|
|
105
|
+
Branch: {branch}
|
|
106
|
+
Commits:
|
|
107
|
+
{log}
|
|
108
|
+
|
|
109
|
+
Diff summary:
|
|
110
|
+
{diff}
|
|
111
|
+
|
|
112
|
+
Format:
|
|
113
|
+
TITLE: <50 char title>
|
|
114
|
+
|
|
115
|
+
DESCRIPTION:
|
|
116
|
+
## Summary
|
|
117
|
+
<2-3 bullet points>
|
|
118
|
+
|
|
119
|
+
## Changes
|
|
120
|
+
<list main changes>
|
|
121
|
+
|
|
122
|
+
Output ONLY in this format:"""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def split (file_diffs: str, branch: str = "", whisper: str = "") -> str:
|
|
126
|
+
return f"""\
|
|
127
|
+
Group these changed files into logical commits. Each group = one commit.
|
|
128
|
+
{_whisper (whisper)}\
|
|
129
|
+
Format: type: message
|
|
130
|
+
Types: feat, fix, refactor, build, chore, docs, test, style, perf, ci
|
|
131
|
+
{_ticket_rule (branch)}
|
|
132
|
+
Rules:
|
|
133
|
+
- Output a JSON array, no markdown fences, no explanation
|
|
134
|
+
- Each element: {{"files": ["path1", "path2"], "message": "type: description"}}
|
|
135
|
+
- ALL LOWERCASE after the colon (except ticket IDs like IMP-123)
|
|
136
|
+
- Imperative mood: "add" not "added", "fix" not "fixes"
|
|
137
|
+
- Max 72 chars per message, no period at end
|
|
138
|
+
- Every file must appear in exactly one group
|
|
139
|
+
- Minimize number of groups (prefer fewer, larger groups)
|
|
140
|
+
- Group by logical change, not by directory
|
|
141
|
+
|
|
142
|
+
Branch: {branch}
|
|
143
|
+
|
|
144
|
+
File diffs:
|
|
145
|
+
{file_diffs}
|
|
146
|
+
|
|
147
|
+
Output ONLY the JSON array:"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def resolve (content: str, path: str, ours: str, theirs: str, whisper: str = "") -> str:
|
|
151
|
+
return f"""\
|
|
152
|
+
Resolve all merge conflicts in this file.
|
|
153
|
+
{_whisper (whisper)}\
|
|
154
|
+
Branches:
|
|
155
|
+
- Ours (current): {ours}
|
|
156
|
+
- Theirs (incoming): {theirs}
|
|
157
|
+
|
|
158
|
+
File: {path}
|
|
159
|
+
|
|
160
|
+
Rules:
|
|
161
|
+
- Resolve every conflict marked by <<<<<<<, =======, >>>>>>>
|
|
162
|
+
- Preserve all non-conflicted code exactly as-is
|
|
163
|
+
- Output the complete resolved file, nothing else
|
|
164
|
+
- No explanation, no markdown fences, no commentary
|
|
165
|
+
|
|
166
|
+
{content}
|
|
167
|
+
|
|
168
|
+
Output ONLY the resolved file:"""
|
imp/theme.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass (frozen=True)
|
|
5
|
+
class Theme:
|
|
6
|
+
accent: str = "#1eff00"
|
|
7
|
+
success: str = "#00ff00"
|
|
8
|
+
error: str = "#ff3131"
|
|
9
|
+
warning: str = "#ff8000"
|
|
10
|
+
muted: str = "#4e7a4e"
|
|
11
|
+
highlight: str = "#a335ee"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
theme = Theme ()
|
imp/validate.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
_COMMIT_RE = re.compile (
|
|
4
|
+
r"^(feat|fix|refactor|build|chore|docs|test|style|perf|ci)"
|
|
5
|
+
r"(\(.+\))?!?: .+"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
_TICKET_RE = re.compile (r"^[A-Z]+-[0-9]")
|
|
9
|
+
|
|
10
|
+
_BRANCH_RE = re.compile (r"^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def commit (msg: str) -> bool:
|
|
14
|
+
subject = msg.split ("\n", 1) [0]
|
|
15
|
+
|
|
16
|
+
if not _COMMIT_RE.match (subject):
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
parts = subject.split (": ", 1)
|
|
20
|
+
if len (parts) < 2 or not parts [1]:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
desc = parts [1]
|
|
24
|
+
return not (desc [0].isupper () and not _TICKET_RE.match (desc))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def branch (name: str) -> bool:
|
|
28
|
+
return bool (_BRANCH_RE.match (name))
|