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/commands/status.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from imp import console, git
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _file_style (code: str) -> str:
|
|
5
|
+
if code in ("M ", "AM"):
|
|
6
|
+
return "green"
|
|
7
|
+
if code == " M" or code == "MM":
|
|
8
|
+
return "yellow"
|
|
9
|
+
if code.strip () == "D":
|
|
10
|
+
return "red"
|
|
11
|
+
if code.startswith ("A"):
|
|
12
|
+
return "green"
|
|
13
|
+
if code.startswith ("R"):
|
|
14
|
+
return f"{console.theme.accent}"
|
|
15
|
+
if code == "??":
|
|
16
|
+
return "dim"
|
|
17
|
+
return "default"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def status ():
|
|
21
|
+
"""Show repository overview.
|
|
22
|
+
|
|
23
|
+
Displays the current branch, file changes with line-level stats,
|
|
24
|
+
commits since the last tag, worktrees, and the last release version.
|
|
25
|
+
Suggests a next action based on the current state.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
git.require ()
|
|
29
|
+
|
|
30
|
+
name = git.repo_name ()
|
|
31
|
+
b = git.branch ()
|
|
32
|
+
tag = git.last_tag ()
|
|
33
|
+
|
|
34
|
+
console.header (name)
|
|
35
|
+
|
|
36
|
+
console.label ("Branch")
|
|
37
|
+
console.item (b)
|
|
38
|
+
console.out.print ()
|
|
39
|
+
|
|
40
|
+
changes = git.status_short ()
|
|
41
|
+
if changes:
|
|
42
|
+
numstat_raw = git.diff_numstat ()
|
|
43
|
+
stats = {}
|
|
44
|
+
for line in numstat_raw.splitlines ():
|
|
45
|
+
parts = line.split ("\t")
|
|
46
|
+
if len (parts) == 3:
|
|
47
|
+
added, removed, path = parts
|
|
48
|
+
stats [path] = (added, removed)
|
|
49
|
+
|
|
50
|
+
console.label ("Changes")
|
|
51
|
+
for line in changes.splitlines ():
|
|
52
|
+
if len (line) < 4:
|
|
53
|
+
continue
|
|
54
|
+
code = line [:2]
|
|
55
|
+
path = line [2:].lstrip (" ")
|
|
56
|
+
style = _file_style (code)
|
|
57
|
+
|
|
58
|
+
stat_str = ""
|
|
59
|
+
if path in stats:
|
|
60
|
+
a, r = stats [path]
|
|
61
|
+
stat_str = f" [green]+{a}[/green] [red]-{r}[/red]"
|
|
62
|
+
|
|
63
|
+
console.out.print (f" [{style}]{code.strip ()}[/{style}] {path}{stat_str}")
|
|
64
|
+
console.out.print ()
|
|
65
|
+
|
|
66
|
+
if tag:
|
|
67
|
+
unpushed = git.log_oneline (rev_range=f"{tag}..HEAD")
|
|
68
|
+
else:
|
|
69
|
+
unpushed = git.log_oneline (count=10)
|
|
70
|
+
|
|
71
|
+
if unpushed:
|
|
72
|
+
count = len (unpushed.splitlines ())
|
|
73
|
+
console.items (f"Commits since {tag or 'start'} ({count})", unpushed)
|
|
74
|
+
|
|
75
|
+
wt = git.worktree_list ()
|
|
76
|
+
wt_lines = wt.splitlines () if wt else []
|
|
77
|
+
if len (wt_lines) > 1:
|
|
78
|
+
console.label ("Worktrees")
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
cwd = str (Path.cwd ())
|
|
81
|
+
for line in wt_lines:
|
|
82
|
+
if line.startswith (cwd + " "):
|
|
83
|
+
console.item (f"{line} (here)")
|
|
84
|
+
else:
|
|
85
|
+
console.item (line)
|
|
86
|
+
console.out.print ()
|
|
87
|
+
|
|
88
|
+
if tag:
|
|
89
|
+
console.muted (f"Last release: {tag}")
|
|
90
|
+
|
|
91
|
+
if changes:
|
|
92
|
+
console.hint ("git add <files>, then imp commit")
|
|
93
|
+
elif unpushed:
|
|
94
|
+
console.hint ("imp release to ship")
|
|
95
|
+
else:
|
|
96
|
+
console.hint ("make changes, then imp commit")
|
imp/commands/sync.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from imp import console, git
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sync (
|
|
7
|
+
yes: bool = typer.Option (False, "--yes", "-y", help="Push without confirmation"),
|
|
8
|
+
):
|
|
9
|
+
"""Pull, rebase, and push current branch.
|
|
10
|
+
|
|
11
|
+
Fetches from origin, rebases local commits on top of upstream changes,
|
|
12
|
+
and offers to push. Requires a clean working tree and an upstream
|
|
13
|
+
tracking branch. Aborts cleanly if the rebase fails.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
git.require ()
|
|
17
|
+
git.require_clean ("imp commit first")
|
|
18
|
+
|
|
19
|
+
console.header ("Sync")
|
|
20
|
+
|
|
21
|
+
b = git.branch ()
|
|
22
|
+
|
|
23
|
+
if not git.has_upstream ():
|
|
24
|
+
console.err ("No upstream branch")
|
|
25
|
+
console.hint (f"git push -u origin {b}")
|
|
26
|
+
raise typer.Exit (1)
|
|
27
|
+
|
|
28
|
+
console.label ("Branch")
|
|
29
|
+
console.item (b)
|
|
30
|
+
console.out.print ()
|
|
31
|
+
|
|
32
|
+
console.spin ("Fetching...", git.fetch)
|
|
33
|
+
|
|
34
|
+
behind = git.count_behind ()
|
|
35
|
+
ahead = git.count_ahead ()
|
|
36
|
+
|
|
37
|
+
if behind == 0 and ahead == 0:
|
|
38
|
+
console.success ("Already up to date")
|
|
39
|
+
raise typer.Exit (0)
|
|
40
|
+
|
|
41
|
+
if behind > 0:
|
|
42
|
+
console.label ("Behind")
|
|
43
|
+
console.item (f"{behind} commits")
|
|
44
|
+
|
|
45
|
+
if ahead > 0:
|
|
46
|
+
console.label ("Ahead")
|
|
47
|
+
console.item (f"{ahead} commits")
|
|
48
|
+
|
|
49
|
+
console.out.print ()
|
|
50
|
+
|
|
51
|
+
if behind > 0:
|
|
52
|
+
console.muted ("Rebasing...")
|
|
53
|
+
if not git.rebase ():
|
|
54
|
+
console.err ("Rebase failed")
|
|
55
|
+
console.hint ("imp resolve to fix conflicts, or git rebase --abort")
|
|
56
|
+
raise typer.Exit (1)
|
|
57
|
+
console.success ("Rebased")
|
|
58
|
+
|
|
59
|
+
if ahead > 0 or behind > 0:
|
|
60
|
+
if yes or console.confirm ("Push?"):
|
|
61
|
+
git.push ()
|
|
62
|
+
console.success ("Pushed")
|
|
63
|
+
|
|
64
|
+
console.hint ("imp status to see overview")
|
imp/commands/undo.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from imp import console, git
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def undo (
|
|
7
|
+
count: int | None = typer.Argument (1, help="Number of commits to undo"),
|
|
8
|
+
):
|
|
9
|
+
"""Undo last N commits, keeping changes staged.
|
|
10
|
+
|
|
11
|
+
Soft-resets HEAD back by N commits so the changes remain staged and
|
|
12
|
+
ready to recommit. Warns if any of the commits have already been
|
|
13
|
+
pushed to the remote.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
git.require ()
|
|
17
|
+
|
|
18
|
+
console.header ("Undo")
|
|
19
|
+
|
|
20
|
+
if git.commit_count () == 0:
|
|
21
|
+
console.muted ("No commits to undo")
|
|
22
|
+
raise typer.Exit (0)
|
|
23
|
+
|
|
24
|
+
total = git.commit_count ()
|
|
25
|
+
if count > total:
|
|
26
|
+
console.err (f"Only {total} commits available")
|
|
27
|
+
raise typer.Exit (1)
|
|
28
|
+
|
|
29
|
+
log = git.log_oneline (count=count)
|
|
30
|
+
console.items ("Commits to undo", log)
|
|
31
|
+
|
|
32
|
+
if git.has_upstream ():
|
|
33
|
+
ahead = git.count_ahead ()
|
|
34
|
+
if count > ahead:
|
|
35
|
+
console.warn ("Some commits are already pushed")
|
|
36
|
+
console.out.print ()
|
|
37
|
+
|
|
38
|
+
if not console.confirm (f"Undo {count} commit(s)? (changes stay staged)"):
|
|
39
|
+
console.muted ("Cancelled")
|
|
40
|
+
raise typer.Exit (0)
|
|
41
|
+
|
|
42
|
+
git.reset (f"HEAD~{count}", soft=True)
|
|
43
|
+
|
|
44
|
+
console.success (f"Undone {count} commit(s)")
|
|
45
|
+
console.hint ("imp commit to recommit, or keep editing")
|
imp/config.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
_DEFAULTS = {
|
|
6
|
+
"provider": "claude",
|
|
7
|
+
"model:fast": "haiku",
|
|
8
|
+
"model:smart": "sonnet",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_ENV_OVERRIDES = {
|
|
12
|
+
"provider": "IMP_AI_PROVIDER",
|
|
13
|
+
"model:fast": "IMP_AI_MODEL_FAST",
|
|
14
|
+
"model:smart": "IMP_AI_MODEL_SMART",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def path () -> Path:
|
|
19
|
+
xdg = os.environ.get ("XDG_CONFIG_HOME", "")
|
|
20
|
+
if not xdg:
|
|
21
|
+
xdg = str (Path.home () / ".config")
|
|
22
|
+
return Path (xdg) / "imp" / "config.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load () -> dict:
|
|
26
|
+
cfg = dict (_DEFAULTS)
|
|
27
|
+
|
|
28
|
+
p = path ()
|
|
29
|
+
if p.is_file ():
|
|
30
|
+
try:
|
|
31
|
+
stored = json.loads (p.read_text ())
|
|
32
|
+
cfg.update (stored)
|
|
33
|
+
except (json.JSONDecodeError, OSError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
for key, env in _ENV_OVERRIDES.items ():
|
|
37
|
+
val = os.environ.get (env, "")
|
|
38
|
+
if val:
|
|
39
|
+
cfg [key] = val
|
|
40
|
+
|
|
41
|
+
return cfg
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def save (cfg: dict):
|
|
45
|
+
p = path ()
|
|
46
|
+
p.parent.mkdir (parents=True, exist_ok=True)
|
|
47
|
+
p.write_text (json.dumps (cfg, indent=3) + "\n")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get (key: str) -> str:
|
|
51
|
+
return load ().get (key, _DEFAULTS.get (key, ""))
|
imp/console.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import tempfile
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
import questionary
|
|
8
|
+
import typer
|
|
9
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.theme import Theme as RichTheme
|
|
14
|
+
|
|
15
|
+
from imp.theme import theme
|
|
16
|
+
|
|
17
|
+
T = TypeVar ("T")
|
|
18
|
+
|
|
19
|
+
_rich_theme = RichTheme ({
|
|
20
|
+
"accent": f"bold {theme.accent}",
|
|
21
|
+
"success": theme.success,
|
|
22
|
+
"error": f"bold {theme.error}",
|
|
23
|
+
"warning": theme.warning,
|
|
24
|
+
"muted": theme.muted,
|
|
25
|
+
"highlight": theme.highlight,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
out = Console (theme=_rich_theme)
|
|
29
|
+
|
|
30
|
+
_pt_style = PTStyle ([
|
|
31
|
+
("qmark", theme.accent),
|
|
32
|
+
("question", f"bold {theme.accent}"),
|
|
33
|
+
("pointer", f"bold {theme.highlight}"),
|
|
34
|
+
("highlighted", f"bold {theme.highlight}"),
|
|
35
|
+
("selected", theme.accent),
|
|
36
|
+
("answer", f"bold {theme.accent}"),
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def header (title: str):
|
|
41
|
+
out.print ()
|
|
42
|
+
out.print (f"[accent]{title}[/accent]")
|
|
43
|
+
out.print ()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def label (text: str):
|
|
47
|
+
out.print (f"[{theme.accent}]{text}[/{theme.accent}]")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def item (text: str):
|
|
51
|
+
out.print (f" [muted]{text}[/muted]")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def items (title: str, data: str):
|
|
55
|
+
label (title)
|
|
56
|
+
for line in data.splitlines ():
|
|
57
|
+
if line.strip ():
|
|
58
|
+
item (line)
|
|
59
|
+
out.print ()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def divider ():
|
|
63
|
+
out.print (
|
|
64
|
+
"[muted]────────────────────────────────────────[/muted]"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def success (msg: str):
|
|
69
|
+
out.print (f"[success]✓[/success] {msg}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def err (msg: str):
|
|
73
|
+
out.print (Panel (
|
|
74
|
+
msg,
|
|
75
|
+
border_style=theme.error,
|
|
76
|
+
title="Error",
|
|
77
|
+
title_align="left",
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def fatal (msg: str):
|
|
82
|
+
err (msg)
|
|
83
|
+
raise typer.Exit (1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def warn (msg: str):
|
|
87
|
+
out.print (f"[warning]{msg}[/warning]")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def hint (msg: str):
|
|
91
|
+
out.print ()
|
|
92
|
+
out.print (f"[muted]→ {msg}[/muted]")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def muted (msg: str):
|
|
96
|
+
out.print (f"[muted]{msg}[/muted]")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def md (text: str):
|
|
100
|
+
out.print (Markdown (text.strip ()))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def review (text: str) -> str:
|
|
104
|
+
panel = Panel (
|
|
105
|
+
text,
|
|
106
|
+
border_style=theme.accent,
|
|
107
|
+
padding=(1, 2),
|
|
108
|
+
)
|
|
109
|
+
out.print (panel)
|
|
110
|
+
out.print ()
|
|
111
|
+
|
|
112
|
+
return choose ("Use this message?", [ "Yes", "Edit", "No" ])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def confirm (msg: str) -> bool:
|
|
116
|
+
return choose (msg, [ "Yes", "No" ]) == "Yes"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def choose (title: str, options: list [str]) -> str:
|
|
120
|
+
result = questionary.select (
|
|
121
|
+
title,
|
|
122
|
+
choices=options,
|
|
123
|
+
style=_pt_style,
|
|
124
|
+
qmark="▸",
|
|
125
|
+
pointer="▸",
|
|
126
|
+
use_arrow_keys=True,
|
|
127
|
+
use_jk_keys=False,
|
|
128
|
+
).ask ()
|
|
129
|
+
|
|
130
|
+
if result is None:
|
|
131
|
+
return options [-1]
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def prompt (label: str, placeholder: str = "") -> str:
|
|
137
|
+
result = questionary.text (
|
|
138
|
+
label,
|
|
139
|
+
default=placeholder,
|
|
140
|
+
style=_pt_style,
|
|
141
|
+
qmark="▸",
|
|
142
|
+
).ask ()
|
|
143
|
+
|
|
144
|
+
return result or ""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def edit (text: str) -> str:
|
|
148
|
+
import os
|
|
149
|
+
|
|
150
|
+
editor = os.environ.get ("EDITOR", "vim")
|
|
151
|
+
with tempfile.NamedTemporaryFile (
|
|
152
|
+
mode="w",
|
|
153
|
+
suffix=".md",
|
|
154
|
+
delete=False,
|
|
155
|
+
) as f:
|
|
156
|
+
f.write (text)
|
|
157
|
+
path = Path (f.name)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
subprocess.run ([ editor, str (path) ], check=True)
|
|
161
|
+
return path.read_text ()
|
|
162
|
+
finally:
|
|
163
|
+
path.unlink (missing_ok=True)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def spin (title: str, fn: Callable [..., T], *args: Any) -> T:
|
|
167
|
+
with out.status (f"[accent]{title}[/accent]", spinner="dots"):
|
|
168
|
+
return fn (*args)
|