gitwise-cli 0.28.0__py3-none-any.whl → 0.29.0__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.
- gitwise/__init__.py +1 -1
- gitwise/_cli_dispatch.py +2 -0
- gitwise/_cli_parser.py +10 -1
- gitwise/_i18n_data.json +28 -0
- gitwise/commit.py +23 -0
- gitwise/merge.py +77 -1
- gitwise/share/schemas/v1/input/merge.json +12 -5
- gitwise/status.py +10 -0
- gitwise/suggest.py +22 -0
- gitwise/utils/git_output.py +11 -0
- gitwise/utils/in_progress.py +112 -0
- gitwise/utils/json_envelope.py +17 -0
- gitwise/utils/parsing.py +5 -0
- {gitwise_cli-0.28.0.dist-info → gitwise_cli-0.29.0.dist-info}/METADATA +1 -1
- {gitwise_cli-0.28.0.dist-info → gitwise_cli-0.29.0.dist-info}/RECORD +18 -17
- {gitwise_cli-0.28.0.dist-info → gitwise_cli-0.29.0.dist-info}/WHEEL +0 -0
- {gitwise_cli-0.28.0.dist-info → gitwise_cli-0.29.0.dist-info}/entry_points.txt +0 -0
- {gitwise_cli-0.28.0.dist-info → gitwise_cli-0.29.0.dist-info}/licenses/LICENSE +0 -0
gitwise/__init__.py
CHANGED
gitwise/_cli_dispatch.py
CHANGED
gitwise/_cli_parser.py
CHANGED
|
@@ -269,11 +269,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
269
269
|
p.add_argument("--yes", "-y", action="store_true", help="skip confirmation")
|
|
270
270
|
|
|
271
271
|
p = sub.add_parser("merge", help="merge/rebase with pre-flight checks", parents=[parent])
|
|
272
|
-
p.add_argument(
|
|
272
|
+
p.add_argument(
|
|
273
|
+
"branch", nargs="?", help="branch to merge/rebase from (omit with --abort/--continue)"
|
|
274
|
+
)
|
|
273
275
|
p.add_argument("--rebase", action="store_true", help="rebase instead of merge")
|
|
274
276
|
p.add_argument("--no-ff", action="store_true", help="force no-fast-forward")
|
|
275
277
|
p.add_argument("--dry-run", action="store_true", help="show checks without merging")
|
|
276
278
|
p.add_argument("--yes", "-y", action="store_true", help="skip confirmation")
|
|
279
|
+
p.add_argument("--abort", action="store_true", help="abort an in-progress merge or rebase")
|
|
280
|
+
p.add_argument(
|
|
281
|
+
"--continue",
|
|
282
|
+
dest="continue_merge",
|
|
283
|
+
action="store_true",
|
|
284
|
+
help="continue an in-progress merge or rebase after resolving conflicts",
|
|
285
|
+
)
|
|
277
286
|
|
|
278
287
|
p = sub.add_parser(
|
|
279
288
|
"conflicts", help="conflict detection and resolution helper", parents=[parent]
|
gitwise/_i18n_data.json
CHANGED
|
@@ -251,6 +251,10 @@
|
|
|
251
251
|
"es": "mensaje",
|
|
252
252
|
"en": "msg"
|
|
253
253
|
},
|
|
254
|
+
"commit_blocked_in_progress": {
|
|
255
|
+
"es": "No se puede commitear: hay una operación {state} en curso. Resuélvela o aborta primero.",
|
|
256
|
+
"en": "Cannot commit: a {state} operation is in progress. Resolve or abort it first."
|
|
257
|
+
},
|
|
254
258
|
"commit_no_message": {
|
|
255
259
|
"es": "usa -m para proveer un mensaje de commit",
|
|
256
260
|
"en": "use -m to provide a commit message"
|
|
@@ -847,6 +851,26 @@
|
|
|
847
851
|
"es": "merge",
|
|
848
852
|
"en": "merge"
|
|
849
853
|
},
|
|
854
|
+
"merge_aborted": {
|
|
855
|
+
"es": "Mezcla abortada",
|
|
856
|
+
"en": "Merge aborted"
|
|
857
|
+
},
|
|
858
|
+
"merge_abort_continue_mutually_exclusive": {
|
|
859
|
+
"es": "--abort y --continue son mutuamente excluyentes",
|
|
860
|
+
"en": "--abort and --continue are mutually exclusive"
|
|
861
|
+
},
|
|
862
|
+
"merge_branch_required": {
|
|
863
|
+
"es": "Especifica la rama a mezclar, o usa --abort/--continue para una operación pausada",
|
|
864
|
+
"en": "Specify a branch to merge, or use --abort/--continue for a paused operation"
|
|
865
|
+
},
|
|
866
|
+
"merge_continued": {
|
|
867
|
+
"es": "Mezcla continuada",
|
|
868
|
+
"en": "Merge continued"
|
|
869
|
+
},
|
|
870
|
+
"merge_no_in_progress": {
|
|
871
|
+
"es": "No hay mezcla/rebase en curso (estado actual: {state}). Usa {action} solo cuando gitwise detecta operación pausada.",
|
|
872
|
+
"en": "No merge/rebase in progress (current state: {state}). Use {action} only when gitwise detects a paused operation."
|
|
873
|
+
},
|
|
850
874
|
"merge_ok": {
|
|
851
875
|
"es": "Mezcla completada: {branch} → {into}",
|
|
852
876
|
"en": "Merge completed: {branch} → {into}"
|
|
@@ -1667,6 +1691,10 @@
|
|
|
1667
1691
|
"es": "Sugerencia: {message}",
|
|
1668
1692
|
"en": "Suggestion: {message}"
|
|
1669
1693
|
},
|
|
1694
|
+
"suggest_blocked_in_progress": {
|
|
1695
|
+
"es": "No se puede sugerir commit: hay una operación {state} en curso. Resuélvela o aborta primero.",
|
|
1696
|
+
"en": "Cannot suggest commit: a {state} operation is in progress. Resolve or abort it first."
|
|
1697
|
+
},
|
|
1670
1698
|
"suggest_no_staged": {
|
|
1671
1699
|
"es": "No hay archivos en área de preparación",
|
|
1672
1700
|
"en": "No staged files"
|
gitwise/commit.py
CHANGED
|
@@ -7,6 +7,7 @@ from .git import PROTECTED_BRANCHES, current_branch, gpg_status, require_root
|
|
|
7
7
|
from .git import run as git_run
|
|
8
8
|
from .i18n import t
|
|
9
9
|
from .output import error, print_bracket, print_header, print_json
|
|
10
|
+
from .utils.in_progress import detect_in_progress, in_progress_hint
|
|
10
11
|
from .utils.json_envelope import error_envelope, ok_envelope
|
|
11
12
|
|
|
12
13
|
_CONVENTIONAL_RE = re.compile(
|
|
@@ -104,12 +105,34 @@ def run_commit(
|
|
|
104
105
|
dry_run: bool = False,
|
|
105
106
|
as_json: bool = False,
|
|
106
107
|
) -> int:
|
|
108
|
+
"""Validate a conventional-commit message and create a GPG-signed commit.
|
|
109
|
+
|
|
110
|
+
Refuses with ``in_progress_<state>`` if an operation is paused. Enforces
|
|
111
|
+
the project's amend policy (no amending pushed or protected branches)
|
|
112
|
+
and GPG readiness before delegating to ``git commit``.
|
|
113
|
+
"""
|
|
107
114
|
root, err = require_root()
|
|
108
115
|
if err:
|
|
109
116
|
return err
|
|
110
117
|
if root is None:
|
|
111
118
|
return 1
|
|
112
119
|
|
|
120
|
+
in_progress = detect_in_progress(root)
|
|
121
|
+
if in_progress["state"] != "none":
|
|
122
|
+
hint = in_progress_hint(in_progress["state"])
|
|
123
|
+
blocked_msg = t("commit_blocked_in_progress", state=in_progress["state"])
|
|
124
|
+
if as_json:
|
|
125
|
+
print_json(
|
|
126
|
+
error_envelope(
|
|
127
|
+
error=blocked_msg,
|
|
128
|
+
code=f"in_progress_{in_progress['state']}",
|
|
129
|
+
hint=hint,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
return 1
|
|
133
|
+
error(blocked_msg, hint=hint)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
113
136
|
amend_policy_rc = _validate_amend_policy(amend=amend, root=root)
|
|
114
137
|
if amend_policy_rc != 0:
|
|
115
138
|
return amend_policy_rc
|
gitwise/merge.py
CHANGED
|
@@ -14,6 +14,7 @@ from .output import (
|
|
|
14
14
|
print_json,
|
|
15
15
|
warn,
|
|
16
16
|
)
|
|
17
|
+
from .utils.in_progress import detect_in_progress
|
|
17
18
|
from .utils.json_envelope import error_envelope, ok_envelope
|
|
18
19
|
from .utils.parsing import to_int
|
|
19
20
|
|
|
@@ -143,21 +144,96 @@ def _report_merge_error(*, as_json: bool, err: str) -> int:
|
|
|
143
144
|
return 1
|
|
144
145
|
|
|
145
146
|
|
|
147
|
+
def _abort_or_continue_args(state: str, abort: bool) -> list[str]:
|
|
148
|
+
"""Build the git argv for resolving a paused merge or rebase.
|
|
149
|
+
|
|
150
|
+
The subcommand must match the paused operation: `git merge --abort` errors
|
|
151
|
+
with "There is no merge to abort" when a rebase is paused, and vice versa.
|
|
152
|
+
Extracted as a pure function so the selection logic is testable without
|
|
153
|
+
driving a real paused rebase.
|
|
154
|
+
"""
|
|
155
|
+
cmd = "rebase" if state == "rebase" else "merge"
|
|
156
|
+
flag = "--abort" if abort else "--continue"
|
|
157
|
+
return [cmd, flag]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _execute_abort_or_continue(*, root: Path, state: str, abort: bool) -> tuple[bool, str]:
|
|
161
|
+
"""Run `git merge/rebase --abort` or `--continue`. Returns (success, error_msg)."""
|
|
162
|
+
result = git_run(_abort_or_continue_args(state=state, abort=abort), cwd=root, check=False)
|
|
163
|
+
if result.returncode == 0:
|
|
164
|
+
return True, ""
|
|
165
|
+
return False, result.stderr.strip() or result.stdout.strip()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _handle_abort_or_continue(*, root: Path, abort: bool, as_json: bool) -> int:
|
|
169
|
+
"""Resolve a paused merge or rebase via `git merge/rebase --abort` / `--continue`."""
|
|
170
|
+
in_progress = detect_in_progress(root)
|
|
171
|
+
if in_progress["state"] not in ("merge", "rebase"):
|
|
172
|
+
available = (
|
|
173
|
+
"git merge/rebase --abort / --continue" if abort else "git merge/rebase --continue"
|
|
174
|
+
)
|
|
175
|
+
msg = t("merge_no_in_progress", action=available, state=in_progress["state"])
|
|
176
|
+
if as_json:
|
|
177
|
+
print_json(error_envelope(error=msg, code="merge_no_in_progress"))
|
|
178
|
+
else:
|
|
179
|
+
error(msg)
|
|
180
|
+
return 1
|
|
181
|
+
success, err_msg = _execute_abort_or_continue(
|
|
182
|
+
root=root, state=in_progress["state"], abort=abort
|
|
183
|
+
)
|
|
184
|
+
if not success:
|
|
185
|
+
return _report_merge_error(as_json=as_json, err=err_msg)
|
|
186
|
+
label_key = "merge_aborted" if abort else "merge_continued"
|
|
187
|
+
if as_json:
|
|
188
|
+
print_json(ok_envelope(action="abort" if abort else "continue"))
|
|
189
|
+
else:
|
|
190
|
+
ok(t(label_key))
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
|
|
146
194
|
def run_merge(
|
|
147
|
-
branch: str,
|
|
195
|
+
branch: str | None = None,
|
|
148
196
|
*,
|
|
149
197
|
rebase: bool = False,
|
|
150
198
|
no_ff: bool = False,
|
|
151
199
|
dry_run: bool = False,
|
|
152
200
|
yes: bool = False,
|
|
201
|
+
abort: bool = False,
|
|
202
|
+
continue_merge: bool = False,
|
|
153
203
|
as_json: bool = False,
|
|
154
204
|
) -> int:
|
|
205
|
+
"""Merge or rebase a branch with pre-flight checks, or resolve a paused op.
|
|
206
|
+
|
|
207
|
+
With ``--abort``/``--continue`` delegates to git's merge/rebase abort and
|
|
208
|
+
continue (subcommand chosen from the detected paused state). Otherwise
|
|
209
|
+
validates the target ref, warns on divergent branches, and asks for
|
|
210
|
+
confirmation before running ``git merge`` (or ``git rebase``).
|
|
211
|
+
"""
|
|
155
212
|
root, err = require_root()
|
|
156
213
|
if err:
|
|
157
214
|
return err
|
|
158
215
|
if root is None:
|
|
159
216
|
return 1
|
|
160
217
|
|
|
218
|
+
if abort and continue_merge:
|
|
219
|
+
msg = t("merge_abort_continue_mutually_exclusive")
|
|
220
|
+
if as_json:
|
|
221
|
+
print_json(error_envelope(error=msg, code="merge_invalid_args"))
|
|
222
|
+
else:
|
|
223
|
+
error(msg)
|
|
224
|
+
return 1
|
|
225
|
+
|
|
226
|
+
if abort or continue_merge:
|
|
227
|
+
return _handle_abort_or_continue(root=root, abort=abort, as_json=as_json)
|
|
228
|
+
|
|
229
|
+
if branch is None:
|
|
230
|
+
msg = t("merge_branch_required")
|
|
231
|
+
if as_json:
|
|
232
|
+
print_json(error_envelope(error=msg, code="merge_branch_required"))
|
|
233
|
+
else:
|
|
234
|
+
error(msg)
|
|
235
|
+
return 1
|
|
236
|
+
|
|
161
237
|
cur = current_branch(root)
|
|
162
238
|
target_err = _validate_merge_target(root=root, branch=branch, cur=cur)
|
|
163
239
|
if target_err is not None:
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"branch": {
|
|
36
36
|
"type": "string",
|
|
37
|
-
"description": "branch to merge/rebase from"
|
|
37
|
+
"description": "branch to merge/rebase from (omit with --abort/--continue)"
|
|
38
38
|
},
|
|
39
39
|
"rebase": {
|
|
40
40
|
"type": "boolean",
|
|
@@ -55,9 +55,16 @@
|
|
|
55
55
|
"type": "boolean",
|
|
56
56
|
"description": "skip confirmation",
|
|
57
57
|
"default": false
|
|
58
|
+
},
|
|
59
|
+
"abort": {
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"description": "abort an in-progress merge or rebase",
|
|
62
|
+
"default": false
|
|
63
|
+
},
|
|
64
|
+
"continue_merge": {
|
|
65
|
+
"type": "boolean",
|
|
66
|
+
"description": "continue an in-progress merge or rebase after resolving conflicts",
|
|
67
|
+
"default": false
|
|
58
68
|
}
|
|
59
|
-
}
|
|
60
|
-
"required": [
|
|
61
|
-
"branch"
|
|
62
|
-
]
|
|
69
|
+
}
|
|
63
70
|
}
|
gitwise/status.py
CHANGED
|
@@ -16,6 +16,7 @@ from .output import (
|
|
|
16
16
|
print_json,
|
|
17
17
|
status,
|
|
18
18
|
)
|
|
19
|
+
from .utils.in_progress import detect_in_progress
|
|
19
20
|
from .utils.parsing import parse_two_ints
|
|
20
21
|
|
|
21
22
|
|
|
@@ -32,6 +33,12 @@ def _range_commits(root: Path, rev_range: str) -> list[dict[str, str]]:
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def run_status(*, as_json: bool = False) -> int:
|
|
36
|
+
"""Print a compact, agent-friendly view of the working-tree state.
|
|
37
|
+
|
|
38
|
+
Exposes branch, ahead/behind counts and commit lists, staged/unstaged/
|
|
39
|
+
untracked file counts, and an ``in_progress`` snapshot of any paused
|
|
40
|
+
merge/rebase/cherry-pick/revert/bisect operation.
|
|
41
|
+
"""
|
|
35
42
|
root, err = require_root()
|
|
36
43
|
if err:
|
|
37
44
|
return err
|
|
@@ -67,6 +74,8 @@ def run_status(*, as_json: bool = False) -> int:
|
|
|
67
74
|
if behind:
|
|
68
75
|
behind_commits = _range_commits(root, "HEAD..@{u}")
|
|
69
76
|
|
|
77
|
+
in_progress = detect_in_progress(root)
|
|
78
|
+
|
|
70
79
|
if as_json:
|
|
71
80
|
print_json(
|
|
72
81
|
{
|
|
@@ -78,6 +87,7 @@ def run_status(*, as_json: bool = False) -> int:
|
|
|
78
87
|
"behind": behind,
|
|
79
88
|
"ahead_commits": ahead_commits,
|
|
80
89
|
"behind_commits": behind_commits,
|
|
90
|
+
"in_progress": in_progress,
|
|
81
91
|
"staged": len(staged),
|
|
82
92
|
"unstaged": len(unstaged),
|
|
83
93
|
"untracked": len(untracked),
|
gitwise/suggest.py
CHANGED
|
@@ -6,6 +6,7 @@ from .git import require_root
|
|
|
6
6
|
from .git import run as git_run
|
|
7
7
|
from .i18n import t
|
|
8
8
|
from .output import error, print_bracket, print_file_status, print_header, print_json, status
|
|
9
|
+
from .utils.in_progress import detect_in_progress, in_progress_hint
|
|
9
10
|
from .utils.json_envelope import error_envelope, ok_envelope
|
|
10
11
|
from .utils.parsing import stripped_non_empty_lines
|
|
11
12
|
|
|
@@ -110,12 +111,33 @@ def _print_suggest_human(
|
|
|
110
111
|
|
|
111
112
|
|
|
112
113
|
def run_suggest(*, as_json: bool = False) -> int:
|
|
114
|
+
"""Inspect staged files and propose a conventional-commit message.
|
|
115
|
+
|
|
116
|
+
Refuses with ``in_progress_<state>`` if a merge/rebase/cherry-pick/revert/
|
|
117
|
+
bisect is paused (so an agent never commits mid-operation).
|
|
118
|
+
"""
|
|
113
119
|
root, err = require_root()
|
|
114
120
|
if err:
|
|
115
121
|
return err
|
|
116
122
|
if root is None:
|
|
117
123
|
return 1
|
|
118
124
|
|
|
125
|
+
in_progress = detect_in_progress(root)
|
|
126
|
+
if in_progress["state"] != "none":
|
|
127
|
+
hint = in_progress_hint(in_progress["state"])
|
|
128
|
+
blocked_msg = t("suggest_blocked_in_progress", state=in_progress["state"])
|
|
129
|
+
if as_json:
|
|
130
|
+
print_json(
|
|
131
|
+
error_envelope(
|
|
132
|
+
error=blocked_msg,
|
|
133
|
+
code=f"in_progress_{in_progress['state']}",
|
|
134
|
+
hint=hint,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
return 1
|
|
138
|
+
error(blocked_msg, hint=hint)
|
|
139
|
+
return 1
|
|
140
|
+
|
|
119
141
|
try:
|
|
120
142
|
with status(t("status_analyzing_staged")):
|
|
121
143
|
staged_files, staged_map = _collect_staged_files(root)
|
gitwise/utils/git_output.py
CHANGED
|
@@ -26,6 +26,12 @@ def status_label(code: str) -> str:
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def parse_diffstat_entries(raw: str, *, default_status: str | None = None) -> list[dict[str, str]]:
|
|
29
|
+
"""Parse `git diff --stat` lines into ``{path, changes[, status]}`` dicts.
|
|
30
|
+
|
|
31
|
+
Lines without ``|`` (summary/blank) are skipped. ``default_status`` is
|
|
32
|
+
attached when the caller knows the change kind (e.g. "staged") but the
|
|
33
|
+
diffstat output does not carry it.
|
|
34
|
+
"""
|
|
29
35
|
entries: list[dict[str, str]] = []
|
|
30
36
|
for line in raw.splitlines():
|
|
31
37
|
if "|" not in line:
|
|
@@ -43,6 +49,11 @@ def parse_diffstat_entries(raw: str, *, default_status: str | None = None) -> li
|
|
|
43
49
|
|
|
44
50
|
|
|
45
51
|
def parse_name_status_entries(raw: str) -> list[dict[str, str]]:
|
|
52
|
+
"""Parse `git diff --name-status` lines into ``{status, path[, code, score, old_path]}`` dicts.
|
|
53
|
+
|
|
54
|
+
Handles R/C (rename/copy) entries whose second column is the old path;
|
|
55
|
+
the score (e.g. ``R100``) is preserved when present.
|
|
56
|
+
"""
|
|
46
57
|
entries: list[dict[str, str]] = []
|
|
47
58
|
for line in raw.splitlines():
|
|
48
59
|
parts = line.split("\t")
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Detect in-progress git operations (merge/rebase/cherry-pick/revert/bisect).
|
|
2
|
+
|
|
3
|
+
Reads the same `.git/` artifacts git itself writes when an operation is
|
|
4
|
+
paused on conflicts or steps. Resolves the real git-dir via
|
|
5
|
+
`git rev-parse --git-dir` so worktrees (which keep their state under
|
|
6
|
+
`.git/worktrees/<name>/`) are handled correctly.
|
|
7
|
+
|
|
8
|
+
Marker reference (Verified against git source: builtin/am.c, sequencer.c,
|
|
9
|
+
builtin/merge.c, git-rebase--merge.sh, gitglossary(7)):
|
|
10
|
+
- MERGE_HEAD → merge paused on conflicts
|
|
11
|
+
- rebase-merge/ → interactive rebase in progress (also `rebase -m`)
|
|
12
|
+
- rebase-apply/ → am/rebase--am in progress
|
|
13
|
+
- CHERRY_PICK_HEAD → cherry-pick paused on conflicts
|
|
14
|
+
- REVERT_HEAD → revert paused on conflicts
|
|
15
|
+
- BISECT_LOG → bisect session active
|
|
16
|
+
- sequencer/todo → multi-step cherry-pick/revert queue
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Literal, TypedDict
|
|
24
|
+
|
|
25
|
+
from ..git import run as git_run
|
|
26
|
+
|
|
27
|
+
InProgressState = Literal["none", "merge", "rebase", "cherry-pick", "revert", "bisect"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InProgressInfo(TypedDict):
|
|
31
|
+
"""Snapshot of any paused git operation in the working tree."""
|
|
32
|
+
|
|
33
|
+
state: InProgressState
|
|
34
|
+
ref: str | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_git_dir(root: Path) -> Path | None:
|
|
38
|
+
"""Return the real git-dir for `root`, or None if git itself is unavailable.
|
|
39
|
+
|
|
40
|
+
Uses `git rev-parse --git-dir` so worktrees resolve to their per-worktree
|
|
41
|
+
state directory rather than the shared common dir.
|
|
42
|
+
"""
|
|
43
|
+
result = git_run(["rev-parse", "--git-dir"], cwd=root, check=False)
|
|
44
|
+
if result.returncode != 0:
|
|
45
|
+
return None
|
|
46
|
+
raw = result.stdout.strip()
|
|
47
|
+
if not raw:
|
|
48
|
+
return None
|
|
49
|
+
# Use os.path.realpath (not Path.resolve()) per AGENTS.md: Path.resolve()
|
|
50
|
+
# can fail on broken symlinks, which matters inside .git/worktrees/.
|
|
51
|
+
candidate = Path(raw)
|
|
52
|
+
git_dir = Path(os.path.realpath(candidate if candidate.is_absolute() else root / raw))
|
|
53
|
+
return git_dir if git_dir.is_dir() else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_head_ref(git_dir: Path, marker: str) -> str | None:
|
|
57
|
+
"""Return the SHA stored in `<git_dir>/<marker>`, or None if absent/empty."""
|
|
58
|
+
marker_path = git_dir / marker
|
|
59
|
+
if not marker_path.is_file():
|
|
60
|
+
return None
|
|
61
|
+
content = marker_path.read_text(encoding="utf-8", errors="replace").strip()
|
|
62
|
+
return content or None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def detect_in_progress(root: Path) -> InProgressInfo:
|
|
66
|
+
"""Inspect `root` for any paused git operation.
|
|
67
|
+
|
|
68
|
+
Returns `{"state": "none", "ref": None}` when the working tree is clean
|
|
69
|
+
of in-progress operations. Priority order: merge > rebase > cherry-pick >
|
|
70
|
+
revert > bisect — matches the order git itself applies when multiple
|
|
71
|
+
state dirs co-exist (which is rare but possible if a user aborts one op
|
|
72
|
+
into another).
|
|
73
|
+
"""
|
|
74
|
+
git_dir = _resolve_git_dir(root)
|
|
75
|
+
if git_dir is None:
|
|
76
|
+
return InProgressInfo(state="none", ref=None)
|
|
77
|
+
|
|
78
|
+
merge_ref = _read_head_ref(git_dir, "MERGE_HEAD")
|
|
79
|
+
if merge_ref is not None:
|
|
80
|
+
return InProgressInfo(state="merge", ref=merge_ref)
|
|
81
|
+
|
|
82
|
+
if (git_dir / "rebase-merge").is_dir() or (git_dir / "rebase-apply").is_dir():
|
|
83
|
+
rebase_ref = _read_head_ref(git_dir / "rebase-merge", "head-name")
|
|
84
|
+
return InProgressInfo(state="rebase", ref=rebase_ref)
|
|
85
|
+
|
|
86
|
+
cherry_ref = _read_head_ref(git_dir, "CHERRY_PICK_HEAD")
|
|
87
|
+
if cherry_ref is not None:
|
|
88
|
+
return InProgressInfo(state="cherry-pick", ref=cherry_ref)
|
|
89
|
+
|
|
90
|
+
revert_ref = _read_head_ref(git_dir, "REVERT_HEAD")
|
|
91
|
+
if revert_ref is not None:
|
|
92
|
+
return InProgressInfo(state="revert", ref=revert_ref)
|
|
93
|
+
|
|
94
|
+
if (git_dir / "BISECT_LOG").is_file():
|
|
95
|
+
return InProgressInfo(state="bisect", ref=None)
|
|
96
|
+
|
|
97
|
+
return InProgressInfo(state="none", ref=None)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_HINT_COMMANDS: dict[InProgressState, str] = {
|
|
101
|
+
"none": "",
|
|
102
|
+
"merge": "git merge --continue # or git merge --abort",
|
|
103
|
+
"rebase": "git rebase --continue # or git rebase --abort",
|
|
104
|
+
"cherry-pick": "git cherry-pick --continue # or git cherry-pick --abort",
|
|
105
|
+
"revert": "git revert --continue # or git revert --abort",
|
|
106
|
+
"bisect": "git bisect reset",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def in_progress_hint(state: InProgressState) -> str:
|
|
111
|
+
"""Return the canonical recovery command for a paused operation state."""
|
|
112
|
+
return _HINT_COMMANDS.get(state, "")
|
gitwise/utils/json_envelope.py
CHANGED
|
@@ -9,6 +9,11 @@ def ok_envelope(
|
|
|
9
9
|
version: int = 2,
|
|
10
10
|
**extra: object,
|
|
11
11
|
) -> dict[str, object]:
|
|
12
|
+
"""Build a success envelope: ``{v, ok: true, ...payload, ...extra}``.
|
|
13
|
+
|
|
14
|
+
``payload`` and ``extra`` are flattened into the top-level dict so callers
|
|
15
|
+
can pass command-specific fields (e.g. ``message=``, ``merged=``) directly.
|
|
16
|
+
"""
|
|
12
17
|
data: dict[str, object] = {}
|
|
13
18
|
if payload is not None:
|
|
14
19
|
data.update(payload)
|
|
@@ -27,6 +32,12 @@ def error_envelope(
|
|
|
27
32
|
version: int = 2,
|
|
28
33
|
**extra: object,
|
|
29
34
|
) -> dict[str, object]:
|
|
35
|
+
"""Build an error envelope: ``{v, ok: false, error, errors: [{code, message, hint?}]}``.
|
|
36
|
+
|
|
37
|
+
``code`` defaults to ``"error"``; pass a stable machine-readable code
|
|
38
|
+
(e.g. ``"in_progress_merge"``) so agents can branch on it. ``hint`` is
|
|
39
|
+
surfaced both in ``errors[0].hint`` and (by callers) in human output.
|
|
40
|
+
"""
|
|
30
41
|
data: dict[str, object] = {}
|
|
31
42
|
if payload is not None:
|
|
32
43
|
data.update(payload)
|
|
@@ -50,6 +61,12 @@ def passthrough_envelope(
|
|
|
50
61
|
version: int = 2,
|
|
51
62
|
**extra: object,
|
|
52
63
|
) -> dict[str, object]:
|
|
64
|
+
"""Build a versioned envelope without an ``ok`` field.
|
|
65
|
+
|
|
66
|
+
Used by commands whose payload already carries the success/failure signal
|
|
67
|
+
(e.g. ``health`` returns its own status), so forcing ``ok: true/false``
|
|
68
|
+
would duplicate or contradict it.
|
|
69
|
+
"""
|
|
53
70
|
data: dict[str, object] = {}
|
|
54
71
|
if payload is not None:
|
|
55
72
|
data.update(payload)
|
gitwise/utils/parsing.py
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def non_empty_lines(text: str) -> list[str]:
|
|
5
|
+
"""Split ``text`` into lines, dropping blanks. Preserves leading/trailing whitespace."""
|
|
5
6
|
return [line for line in text.splitlines() if line.strip()]
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def stripped_non_empty_lines(text: str) -> list[str]:
|
|
10
|
+
"""Split ``text`` into lines, dropping blanks and stripping each survivor."""
|
|
9
11
|
return [line.strip() for line in text.splitlines() if line.strip()]
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def to_int(value: object, *, default: int = 0) -> int:
|
|
15
|
+
"""Coerce ``value`` to int, returning ``default`` on parse failure (no exception)."""
|
|
13
16
|
if isinstance(value, int):
|
|
14
17
|
return value
|
|
15
18
|
try:
|
|
@@ -19,6 +22,7 @@ def to_int(value: object, *, default: int = 0) -> int:
|
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def parse_two_ints(text: str) -> tuple[int, int] | None:
|
|
25
|
+
"""Parse two whitespace-separated ints (e.g. git ahead/behind counts). None on failure."""
|
|
22
26
|
parts = text.strip().split()
|
|
23
27
|
if len(parts) != 2:
|
|
24
28
|
return None
|
|
@@ -29,6 +33,7 @@ def parse_two_ints(text: str) -> tuple[int, int] | None:
|
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
def dict_list(value: object) -> list[dict[str, object]]:
|
|
36
|
+
"""Return ``value`` if it is a list of dicts, else ``[]``. Tolerates non-list input."""
|
|
32
37
|
if not isinstance(value, list):
|
|
33
38
|
return []
|
|
34
39
|
return [item for item in value if isinstance(item, dict)]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitwise-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.29.0
|
|
4
4
|
Summary: Python CLI for optimizing git workflows and Claude Code integration
|
|
5
5
|
Project-URL: Homepage, https://github.com/drzioner/gitwise
|
|
6
6
|
Project-URL: Repository, https://github.com/drzioner/gitwise
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
gitwise/__init__.py,sha256=
|
|
1
|
+
gitwise/__init__.py,sha256=cmAUJH2IftxUFkDcALDoZ2YeQWoy7EdWEOnyWGBlsOA,267
|
|
2
2
|
gitwise/__main__.py,sha256=FqdvhDqIEA1PJQqWJExu3MsyFquWqAhMN-9l6wOsOOI,4353
|
|
3
3
|
gitwise/_cli_completions.py,sha256=85-aF5I-SFFehbSDvrj22eFb3DYiHiNxpwr6GxW0B4Q,3091
|
|
4
|
-
gitwise/_cli_dispatch.py,sha256=
|
|
4
|
+
gitwise/_cli_dispatch.py,sha256=x7lsDQ5YB18Tbu0OiEP62fTXd6EA86MyHMunYKmCVJk,12220
|
|
5
5
|
gitwise/_cli_introspection.py,sha256=wlRMcRrGpitUGs5Sm1g-Eyf0avLeHpYhsBs48PCC2eI,9112
|
|
6
|
-
gitwise/_cli_parser.py,sha256=
|
|
6
|
+
gitwise/_cli_parser.py,sha256=Purtkyrb8Eg7F2WnhHoCmJx7Rb_LWCYtJgBFCH22kpQ,14044
|
|
7
7
|
gitwise/_cli_setup_agents.py,sha256=HC65Y3lzSvzKOz5Rdl9zMOpM5Rc-HS0YU7q8re09RBg,14926
|
|
8
|
-
gitwise/_i18n_data.json,sha256=
|
|
8
|
+
gitwise/_i18n_data.json,sha256=GFJqfLRX6Bl_MKaJnFKivKJ66aG5l3Wb-sDWhqPvs4g,65466
|
|
9
9
|
gitwise/_paths.py,sha256=VRkql9HQ5_OjwbpcX-18fwe_lMpuQffP4AiBV6suSFg,658
|
|
10
10
|
gitwise/_runtime_config.py,sha256=G3MqJwPoKWMak3dZkPHXqCeJPph2_9KBEomQ5YT2RHI,7176
|
|
11
11
|
gitwise/audit.py,sha256=mdovDdJ2PQ4dSTBBNujA0KrYzdqZaOQZt6GrbSW7vfA,10507
|
|
12
12
|
gitwise/branches.py,sha256=qleh7ZuT_XxO1KoEDjCX-thlXTYZeiZg9vsWFOMdZ7k,6006
|
|
13
13
|
gitwise/clean.py,sha256=JubEZV5P43INKRJnXTTtoLjKoVZab0Z4Ph3OYZgctj4,5698
|
|
14
|
-
gitwise/commit.py,sha256=
|
|
14
|
+
gitwise/commit.py,sha256=L7QYo3pJVtV7ccKOg5yH0I2y3KvQtMIfJOoBxabBcj4,4828
|
|
15
15
|
gitwise/conflicts.py,sha256=v3yfbDMHn62m8EUjRrWzDcT-EH0jiNba2gmqFVsnpWQ,3719
|
|
16
16
|
gitwise/context.py,sha256=uC7nwY5phJwTeYQIhxpqDS_RWhvp01oFhKjT-oM4hF8,5525
|
|
17
17
|
gitwise/design.py,sha256=lr7R6PaUTOGNyY8bPHU-soG8FV0Zsf3cBkRZkyUWuBk,11156
|
|
@@ -21,7 +21,7 @@ gitwise/git.py,sha256=dgQhAggiy8-jbP6GX1aeQOGxZWMYaQDZg2i86VRklXI,7570
|
|
|
21
21
|
gitwise/health.py,sha256=y8abwy_tEbv6fhdKU5iVR9yMOupsWj1bodSDfC9eyaI,9616
|
|
22
22
|
gitwise/i18n.py,sha256=9tEI-xs6lUdku3NnqMGwtM7Ck6AlUSm7nrPd95yzXcg,2448
|
|
23
23
|
gitwise/log.py,sha256=e74rOZtSTQe8ea9e1fyygjQPXUuNFUSamPNy7S9pvAs,9210
|
|
24
|
-
gitwise/merge.py,sha256=
|
|
24
|
+
gitwise/merge.py,sha256=9B0QsGXUEUfUy6dpKrXGizMZaBJkhbPD26M-SJtcihY,8767
|
|
25
25
|
gitwise/optimize.py,sha256=Eqka6u2s-eSMKfkhWynAYlEwjjAL4rOwEHoPikxXew8,5955
|
|
26
26
|
gitwise/output.py,sha256=bD8Yjp4tW-D7hw0u3upYqzJIGc3k70gCS5_Au7foosE,18827
|
|
27
27
|
gitwise/pick.py,sha256=2JNkcaL4D7z0dRfFyoDRQsa7_2vGtkBV22MMxHFIYG4,2985
|
|
@@ -32,8 +32,8 @@ gitwise/setup.py,sha256=CbOc8wyRytLQza-v1XkVr0bV29Es6UbDWrh2lQDUfSs,16625
|
|
|
32
32
|
gitwise/show.py,sha256=gFjYbUY560HfSAlACDwyx7j7ST_XRfxe-BnwompPqWw,3851
|
|
33
33
|
gitwise/snapshot.py,sha256=N1TVA27YqJKJBg-a37pZK0U1nDIB07IWG6sj6aa8JfA,3470
|
|
34
34
|
gitwise/stash.py,sha256=J6LvNCXwJvovL3CH8JEYxqajQAb2j33P42D3drXa_e8,5437
|
|
35
|
-
gitwise/status.py,sha256=
|
|
36
|
-
gitwise/suggest.py,sha256=
|
|
35
|
+
gitwise/status.py,sha256=xDs1HmrPjbw7fTbw4v4yt7WoSuJzi_1SDN1tQ9lOpHc,4475
|
|
36
|
+
gitwise/suggest.py,sha256=tya7tzVy4L_v9Mp9xwAckQ84QbPEvINU7O9duRxbjGU,5411
|
|
37
37
|
gitwise/summarize.py,sha256=KIlfG1mqOj71jGY7VMaA0GIlzwhQjAtX6aNDUR2niHo,6710
|
|
38
38
|
gitwise/sync.py,sha256=kpnToYTdwRvdh6NDx-ZCgpqU2Ayz7_uKnudJYAxIV_o,7420
|
|
39
39
|
gitwise/tag.py,sha256=X2AWglsLfFb4chYxg5YNrqmoZmPMbwo8bDBS7X3GgVQ,6495
|
|
@@ -66,9 +66,10 @@ gitwise/setup_agents/providers/cursor.py,sha256=bz2_AboPIFc1gkvsEon6JP659ksgtFiW
|
|
|
66
66
|
gitwise/setup_agents/providers/opencode.py,sha256=ZfA18EP9Pcqn-wStBcBAmFwHcu7PO_mJ68ErztaKZXM,330
|
|
67
67
|
gitwise/setup_agents/providers/pi.py,sha256=-sj_Ufa25OhBbhZ6P3QlKBTLcIVRdKrgCZ6I8NYJ4OM,312
|
|
68
68
|
gitwise/utils/__init__.py,sha256=sW1hu0zxDj57_wkxhxLce2ZRGgHPtn3ey0IBeIw69Xg,42
|
|
69
|
-
gitwise/utils/git_output.py,sha256=
|
|
70
|
-
gitwise/utils/
|
|
71
|
-
gitwise/utils/
|
|
69
|
+
gitwise/utils/git_output.py,sha256=dFp0nM4UrY-npC5bjAaOB-x18Q0M5DUPrNvvNxzIaS0,2877
|
|
70
|
+
gitwise/utils/in_progress.py,sha256=YyC5o-a7KQWW1pnKymoecFk5umJovcA6o-a0E6OZIJ4,4304
|
|
71
|
+
gitwise/utils/json_envelope.py,sha256=OQ2CJCFlmyeRofWXeFynBHeyxh_Cj3dOBhlliDW3xO0,2147
|
|
72
|
+
gitwise/utils/parsing.py,sha256=rPUrdXmfm29gOXnIR0UaxbSLyot2dqS1gcPWsNLuFqU,1391
|
|
72
73
|
gitwise/share/git-config-modern.txt,sha256=CwIu4xVZuGrujWioHq6yjmjfzPNFgSfWHXtL0EmREIs,875
|
|
73
74
|
gitwise/share/agents/skills/git-audit/SKILL.md,sha256=ItlLBBKUcug11dC2_ftRcp1eRSmNHaJpBtMGQXMksFA,1079
|
|
74
75
|
gitwise/share/agents/skills/git-clean/SKILL.md,sha256=vNxY4KJ6hYvSknTiZsTuJx8fZJFdhJ0VwXLJ49PU_6U,930
|
|
@@ -100,7 +101,7 @@ gitwise/share/schemas/v1/input/diff.json,sha256=QAuq15XR3M3UE3dTfSSMd3Dvq2TYz79T
|
|
|
100
101
|
gitwise/share/schemas/v1/input/doctor.json,sha256=qkR-oY8jlNyur8USc62o8g8EmUFA0q3t5sVCSfHO8vE,839
|
|
101
102
|
gitwise/share/schemas/v1/input/health.json,sha256=ujZmveZ8un8DhgKGan79A_MOTxu0rjXXUmDlSAeU7oM,839
|
|
102
103
|
gitwise/share/schemas/v1/input/log.json,sha256=6sWO7QUKaSMEofNgZPDQZ_bv89SkNkExqkz9awO0hPU,1634
|
|
103
|
-
gitwise/share/schemas/v1/input/merge.json,sha256
|
|
104
|
+
gitwise/share/schemas/v1/input/merge.json,sha256=-dYyx7crs7re1TQFa3ssquRd4Xe7zbExZUCu6sIM0Ts,1735
|
|
104
105
|
gitwise/share/schemas/v1/input/optimize.json,sha256=cymFwU8RuHbR54RUh40F__rkHupqYLJb60L6J5hCzNg,983
|
|
105
106
|
gitwise/share/schemas/v1/input/pick.json,sha256=JMuEkIj64ntvXzoXYAh8PA3w6dgO-p0sOS7P0xEFFkE,1472
|
|
106
107
|
gitwise/share/schemas/v1/input/pr.json,sha256=FG_KrZj09f9bQMQOxfnR7_P4IOlIY-jZkbX4_unweOE,1152
|
|
@@ -118,8 +119,8 @@ gitwise/share/schemas/v1/input/tag.json,sha256=KmrCyqcJsJU_uDhNba_KYpAkYg7vIlQac
|
|
|
118
119
|
gitwise/share/schemas/v1/input/undo.json,sha256=PRab96r2e1iU2FN8yBV_njgly_-7lm7Vy4KcMh3NMQ4,1406
|
|
119
120
|
gitwise/share/schemas/v1/input/update.json,sha256=OmuSgSp_USg2f45mywnSpxUB0NMSn_lQDLC4yDzCopo,911
|
|
120
121
|
gitwise/share/schemas/v1/input/worktree.json,sha256=rKKOtWyAgARIa-b-Raw7ZNPqKkO7DlPd7PE1c0flRDs,1063
|
|
121
|
-
gitwise_cli-0.
|
|
122
|
-
gitwise_cli-0.
|
|
123
|
-
gitwise_cli-0.
|
|
124
|
-
gitwise_cli-0.
|
|
125
|
-
gitwise_cli-0.
|
|
122
|
+
gitwise_cli-0.29.0.dist-info/METADATA,sha256=V2xpVXxYq0XvF6kC4_RNTrwvS1FDu9gFfRvYNIWZGxA,7092
|
|
123
|
+
gitwise_cli-0.29.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
124
|
+
gitwise_cli-0.29.0.dist-info/entry_points.txt,sha256=rGwxSDmUHtzlY6yp7KeMJhRARijXkSE6OKuOJ8PF51E,50
|
|
125
|
+
gitwise_cli-0.29.0.dist-info/licenses/LICENSE,sha256=vfJO-ThMtWhZOD9MsArN2yul1EJmxAXxfeGHKnEk9QQ,1063
|
|
126
|
+
gitwise_cli-0.29.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|