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/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)