gitwise-cli 0.28.0__tar.gz → 0.29.0__tar.gz

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.
Files changed (174) hide show
  1. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/CHANGELOG.md +16 -0
  2. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/PKG-INFO +1 -1
  3. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/__init__.py +1 -1
  4. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/_cli_dispatch.py +2 -0
  5. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/_cli_parser.py +10 -1
  6. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/_i18n_data.json +28 -0
  7. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/commit.py +23 -0
  8. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/merge.py +77 -1
  9. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/status.py +10 -0
  10. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/suggest.py +22 -0
  11. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/utils/git_output.py +11 -0
  12. gitwise_cli-0.29.0/gitwise/utils/in_progress.py +112 -0
  13. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/utils/json_envelope.py +17 -0
  14. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/utils/parsing.py +5 -0
  15. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/pyproject.toml +1 -1
  16. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/merge.json +12 -5
  17. gitwise_cli-0.29.0/tests/test_in_progress.py +215 -0
  18. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/.gitignore +0 -0
  19. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/LICENSE +0 -0
  20. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/README.md +0 -0
  21. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/bin/gitwise +0 -0
  22. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/__main__.py +0 -0
  23. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/_cli_completions.py +0 -0
  24. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/_cli_introspection.py +0 -0
  25. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/_cli_setup_agents.py +0 -0
  26. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/_paths.py +0 -0
  27. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/_runtime_config.py +0 -0
  28. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/audit.py +0 -0
  29. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/branches.py +0 -0
  30. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/clean.py +0 -0
  31. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/conflicts.py +0 -0
  32. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/context.py +0 -0
  33. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/design.py +0 -0
  34. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/diff.py +0 -0
  35. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/doctor.py +0 -0
  36. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/git.py +0 -0
  37. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/health.py +0 -0
  38. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/i18n.py +0 -0
  39. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/log.py +0 -0
  40. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/optimize.py +0 -0
  41. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/output.py +0 -0
  42. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/pick.py +0 -0
  43. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/pr.py +0 -0
  44. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/py.typed +0 -0
  45. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/schema.py +0 -0
  46. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup.py +0 -0
  47. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/__init__.py +0 -0
  48. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/adapters/__init__.py +0 -0
  49. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/adapters/aider.py +0 -0
  50. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/adapters/base.py +0 -0
  51. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/adapters/codex.py +0 -0
  52. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/adapters/continue_adapter.py +0 -0
  53. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/adapters/cursor.py +0 -0
  54. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/adapters/opencode.py +0 -0
  55. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/adapters/pi.py +0 -0
  56. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/exec.py +0 -0
  57. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/format.py +0 -0
  58. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/plan.py +0 -0
  59. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/plan_gitfiles.py +0 -0
  60. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/plan_skills.py +0 -0
  61. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/__init__.py +0 -0
  62. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/aider.py +0 -0
  63. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/base.py +0 -0
  64. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/claude.py +0 -0
  65. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/codex.py +0 -0
  66. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/continue_adapter.py +0 -0
  67. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/cursor.py +0 -0
  68. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/opencode.py +0 -0
  69. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/providers/pi.py +0 -0
  70. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/state.py +0 -0
  71. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/setup_agents/types.py +0 -0
  72. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/show.py +0 -0
  73. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/snapshot.py +0 -0
  74. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/stash.py +0 -0
  75. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/summarize.py +0 -0
  76. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/sync.py +0 -0
  77. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/tag.py +0 -0
  78. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/undo.py +0 -0
  79. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/update.py +0 -0
  80. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/utils/__init__.py +0 -0
  81. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/gitwise/worktree.py +0 -0
  82. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/install.sh +0 -0
  83. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/agents/skills/git-audit/SKILL.md +0 -0
  84. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/agents/skills/git-clean/SKILL.md +0 -0
  85. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/agents/skills/git-optimize/SKILL.md +0 -0
  86. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/aider/CONVENTIONS.md.template +0 -0
  87. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/aider/aider.conf.yml.template +0 -0
  88. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/claude/CLAUDE.md.template +0 -0
  89. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/claude/rules/gitwise.md +0 -0
  90. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/claude/settings.json.template +0 -0
  91. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/claude/skills/git-audit/SKILL.md +0 -0
  92. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/claude/skills/git-clean/SKILL.md +0 -0
  93. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/claude/skills/git-optimize/SKILL.md +0 -0
  94. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/codex/agents/gitwise.toml.template +0 -0
  95. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/continue/rules/gitwise.md.template +0 -0
  96. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/cursor/rules/gitwise.mdc.template +0 -0
  97. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/git-config-modern.txt +0 -0
  98. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/hooks/commit-msg +0 -0
  99. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/hooks/pre-commit +0 -0
  100. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/opencode/agents/gitwise.md.template +0 -0
  101. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/pi/skills/gitwise.md.template +0 -0
  102. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/audit.json +0 -0
  103. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/branches.json +0 -0
  104. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/clean.json +0 -0
  105. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/commands.json +0 -0
  106. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/commit.json +0 -0
  107. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/completions.json +0 -0
  108. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/conflicts.json +0 -0
  109. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/context.json +0 -0
  110. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/diff.json +0 -0
  111. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/doctor.json +0 -0
  112. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/health.json +0 -0
  113. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/log.json +0 -0
  114. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/optimize.json +0 -0
  115. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/pick.json +0 -0
  116. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/pr.json +0 -0
  117. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/schema.json +0 -0
  118. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/setup-agents.json +0 -0
  119. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/setup.json +0 -0
  120. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/show.json +0 -0
  121. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/snapshot.json +0 -0
  122. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/stash.json +0 -0
  123. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/status.json +0 -0
  124. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/suggest.json +0 -0
  125. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/summarize.json +0 -0
  126. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/sync.json +0 -0
  127. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/tag.json +0 -0
  128. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/undo.json +0 -0
  129. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/update.json +0 -0
  130. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/share/schemas/v1/input/worktree.json +0 -0
  131. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/__init__.py +0 -0
  132. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/conftest.py +0 -0
  133. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_adapters.py +0 -0
  134. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_audit.py +0 -0
  135. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_branches.py +0 -0
  136. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_clean.py +0 -0
  137. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_commit.py +0 -0
  138. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_conflicts.py +0 -0
  139. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_context.py +0 -0
  140. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_design.py +0 -0
  141. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_diff.py +0 -0
  142. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_doctor.py +0 -0
  143. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_edge_cases.py +0 -0
  144. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_git.py +0 -0
  145. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_git_output_utils.py +0 -0
  146. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_health.py +0 -0
  147. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_i18n.py +0 -0
  148. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_json_envelope_utils.py +0 -0
  149. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_log.py +0 -0
  150. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_main.py +0 -0
  151. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_md_links_script.py +0 -0
  152. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_merge.py +0 -0
  153. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_optimize.py +0 -0
  154. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_output.py +0 -0
  155. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_parsing_utils.py +0 -0
  156. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_pick.py +0 -0
  157. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_pr.py +0 -0
  158. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_roadmap_baseline_script.py +0 -0
  159. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_sa_plan.py +0 -0
  160. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_sa_unit.py +0 -0
  161. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_schema_catalog.py +0 -0
  162. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_setup.py +0 -0
  163. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_setup_agents.py +0 -0
  164. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_show.py +0 -0
  165. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_snapshot.py +0 -0
  166. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_stash.py +0 -0
  167. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_status.py +0 -0
  168. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_suggest.py +0 -0
  169. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_summarize.py +0 -0
  170. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_sync.py +0 -0
  171. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_sync_changelog_es_script.py +0 -0
  172. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_tag.py +0 -0
  173. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_undo.py +0 -0
  174. {gitwise_cli-0.28.0 → gitwise_cli-0.29.0}/tests/test_worktree.py +0 -0
@@ -3,6 +3,22 @@
3
3
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## v0.29.0 (2026-06-19)
7
+
8
+ ### Feat
9
+
10
+ - in-progress safety + merge --abort/--continue (Sprint 1) (#65)
11
+ - detect in-progress operations and guard suggest/commit (Sprint 1)
12
+
13
+ ### Fix
14
+
15
+ - **in_progress**: use os.path.realpath and explicit subprocess timeout (CodeRabbit)
16
+ - **merge**: pick rebase subcommand when state=rebase (gemini PR#65 finding)
17
+
18
+ ### Refactor
19
+
20
+ - **merge**: extract _handle_abort_or_continue (Sprint 1 multi-review)
21
+
6
22
  ## v0.28.0 (2026-06-19)
7
23
 
8
24
  ### BREAKING CHANGE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitwise-cli
3
- Version: 0.28.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,4 +1,4 @@
1
- __version__ = "0.28.0"
1
+ __version__ = "0.29.0"
2
2
 
3
3
 
4
4
  def get_version() -> str:
@@ -255,6 +255,8 @@ def _run_merge(args: argparse.Namespace) -> int:
255
255
  no_ff=args.no_ff,
256
256
  dry_run=args.dry_run,
257
257
  yes=args.yes,
258
+ abort=args.abort,
259
+ continue_merge=args.continue_merge,
258
260
  as_json=args.json,
259
261
  )
260
262
 
@@ -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("branch", help="branch to merge/rebase from")
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]
@@ -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"
@@ -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
@@ -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:
@@ -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),
@@ -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)
@@ -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, "")
@@ -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)
@@ -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
  [project]
2
2
  name = "gitwise-cli"
3
- version = "0.28.0"
3
+ version = "0.29.0"
4
4
  description = "Python CLI for optimizing git workflows and Claude Code integration"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
  }