gitwise-cli 0.32.0__tar.gz → 0.33.1__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 (185) hide show
  1. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/CHANGELOG.md +12 -0
  2. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/PKG-INFO +16 -3
  3. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/README.md +14 -1
  4. gitwise_cli-0.33.1/bin/gw +5 -0
  5. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/__init__.py +1 -1
  6. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/_cli_completions.py +76 -1
  7. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/_cli_dispatch.py +3 -1
  8. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/_cli_parser.py +25 -5
  9. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/_i18n_data.json +24 -0
  10. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/conflicts.py +79 -2
  11. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/diff.py +37 -3
  12. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/log.py +27 -5
  13. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/output.py +8 -0
  14. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/worktree.py +26 -1
  15. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/pyproject.toml +3 -2
  16. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/claude/rules/gitwise.md +1 -1
  17. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/claude/settings.json.template +1 -0
  18. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/codex/agents/gitwise.toml.template +1 -1
  19. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/completions.json +2 -1
  20. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/conflicts.json +5 -0
  21. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/diff.json +5 -0
  22. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/log.json +5 -0
  23. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/worktree.json +2 -1
  24. gitwise_cli-0.33.1/tests/test_conflicts.py +86 -0
  25. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_diff.py +21 -0
  26. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_log.py +18 -0
  27. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_main.py +16 -0
  28. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_worktree.py +25 -0
  29. gitwise_cli-0.32.0/tests/test_conflicts.py +0 -45
  30. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/.gitignore +0 -0
  31. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/LICENSE +0 -0
  32. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/bin/gitwise +0 -0
  33. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/__main__.py +0 -0
  34. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/_cli_introspection.py +0 -0
  35. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/_cli_setup_agents.py +0 -0
  36. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/_paths.py +0 -0
  37. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/_runtime_config.py +0 -0
  38. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/audit.py +0 -0
  39. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/branches.py +0 -0
  40. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/clean.py +0 -0
  41. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/commit.py +0 -0
  42. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/context.py +0 -0
  43. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/design.py +0 -0
  44. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/doctor.py +0 -0
  45. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/git.py +0 -0
  46. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/health.py +0 -0
  47. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/i18n.py +0 -0
  48. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/merge.py +0 -0
  49. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/optimize.py +0 -0
  50. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/pick.py +0 -0
  51. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/pr.py +0 -0
  52. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/py.typed +0 -0
  53. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/schema.py +0 -0
  54. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup.py +0 -0
  55. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/__init__.py +0 -0
  56. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/adapters/__init__.py +0 -0
  57. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/adapters/aider.py +0 -0
  58. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/adapters/base.py +0 -0
  59. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/adapters/codex.py +0 -0
  60. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/adapters/continue_adapter.py +0 -0
  61. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/adapters/cursor.py +0 -0
  62. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/adapters/opencode.py +0 -0
  63. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/adapters/pi.py +0 -0
  64. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/exec.py +0 -0
  65. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/format.py +0 -0
  66. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/plan.py +0 -0
  67. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/plan_gitfiles.py +0 -0
  68. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/plan_skills.py +0 -0
  69. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/__init__.py +0 -0
  70. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/aider.py +0 -0
  71. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/base.py +0 -0
  72. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/claude.py +0 -0
  73. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/codex.py +0 -0
  74. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/continue_adapter.py +0 -0
  75. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/cursor.py +0 -0
  76. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/opencode.py +0 -0
  77. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/providers/pi.py +0 -0
  78. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/state.py +0 -0
  79. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/setup_agents/types.py +0 -0
  80. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/show.py +0 -0
  81. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/snapshot.py +0 -0
  82. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/stash.py +0 -0
  83. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/status.py +0 -0
  84. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/suggest.py +0 -0
  85. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/summarize.py +0 -0
  86. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/sync.py +0 -0
  87. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/tag.py +0 -0
  88. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/undo.py +0 -0
  89. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/update.py +0 -0
  90. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/utils/__init__.py +0 -0
  91. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/utils/git_output.py +0 -0
  92. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/utils/in_progress.py +0 -0
  93. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/utils/json_envelope.py +0 -0
  94. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/utils/parsing.py +0 -0
  95. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/utils/secret_scan.py +0 -0
  96. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/gitwise/utils/types.py +0 -0
  97. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/install.sh +0 -0
  98. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/agents/skills/git-audit/SKILL.md +0 -0
  99. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/agents/skills/git-clean/SKILL.md +0 -0
  100. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/agents/skills/git-optimize/SKILL.md +0 -0
  101. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/aider/CONVENTIONS.md.template +0 -0
  102. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/aider/aider.conf.yml.template +0 -0
  103. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/claude/CLAUDE.md.template +0 -0
  104. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/claude/skills/git-audit/SKILL.md +0 -0
  105. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/claude/skills/git-clean/SKILL.md +0 -0
  106. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/claude/skills/git-optimize/SKILL.md +0 -0
  107. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/continue/rules/gitwise.md.template +0 -0
  108. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/cursor/rules/gitwise.mdc.template +0 -0
  109. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/git-config-modern.txt +0 -0
  110. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/hooks/commit-msg +0 -0
  111. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/hooks/pre-commit +0 -0
  112. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/opencode/agents/gitwise.md.template +0 -0
  113. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/pi/skills/gitwise.md.template +0 -0
  114. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/audit.json +0 -0
  115. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/branches.json +0 -0
  116. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/clean.json +0 -0
  117. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/commands.json +0 -0
  118. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/commit.json +0 -0
  119. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/context.json +0 -0
  120. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/doctor.json +0 -0
  121. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/health.json +0 -0
  122. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/merge.json +0 -0
  123. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/optimize.json +0 -0
  124. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/pick.json +0 -0
  125. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/pr.json +0 -0
  126. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/schema.json +0 -0
  127. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/setup-agents.json +0 -0
  128. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/setup.json +0 -0
  129. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/show.json +0 -0
  130. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/snapshot.json +0 -0
  131. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/stash.json +0 -0
  132. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/status.json +0 -0
  133. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/suggest.json +0 -0
  134. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/summarize.json +0 -0
  135. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/sync.json +0 -0
  136. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/tag.json +0 -0
  137. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/undo.json +0 -0
  138. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/input/update.json +0 -0
  139. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/output/diff.json +0 -0
  140. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/output/log.json +0 -0
  141. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/output/setup-agents.json +0 -0
  142. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/share/schemas/v1/output/status.json +0 -0
  143. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/__init__.py +0 -0
  144. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/conftest.py +0 -0
  145. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_adapters.py +0 -0
  146. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_audit.py +0 -0
  147. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_branches.py +0 -0
  148. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_clean.py +0 -0
  149. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_commit.py +0 -0
  150. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_context.py +0 -0
  151. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_design.py +0 -0
  152. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_doctor.py +0 -0
  153. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_edge_cases.py +0 -0
  154. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_envelope_contract.py +0 -0
  155. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_git.py +0 -0
  156. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_git_output_utils.py +0 -0
  157. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_health.py +0 -0
  158. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_i18n.py +0 -0
  159. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_in_progress.py +0 -0
  160. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_json_envelope_utils.py +0 -0
  161. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_md_links_script.py +0 -0
  162. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_merge.py +0 -0
  163. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_optimize.py +0 -0
  164. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_output.py +0 -0
  165. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_parsing_utils.py +0 -0
  166. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_pick.py +0 -0
  167. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_pr.py +0 -0
  168. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_roadmap_baseline_script.py +0 -0
  169. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_sa_plan.py +0 -0
  170. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_sa_unit.py +0 -0
  171. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_schema_catalog.py +0 -0
  172. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_secret_scan.py +0 -0
  173. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_setup.py +0 -0
  174. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_setup_agents.py +0 -0
  175. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_show.py +0 -0
  176. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_snapshot.py +0 -0
  177. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_stash.py +0 -0
  178. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_status.py +0 -0
  179. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_suggest.py +0 -0
  180. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_summarize.py +0 -0
  181. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_sync.py +0 -0
  182. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_sync_changelog_es_script.py +0 -0
  183. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_tag.py +0 -0
  184. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_types.py +0 -0
  185. {gitwise_cli-0.32.0 → gitwise_cli-0.33.1}/tests/test_undo.py +0 -0
@@ -3,6 +3,18 @@
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.33.1 (2026-06-23)
7
+
8
+ ### Fix
9
+
10
+ - make gw a real binary for non-interactive shells (#80)
11
+
12
+ ## v0.33.0 (2026-06-20)
13
+
14
+ ### Feat
15
+
16
+ - Sprint 4 DX polish -- worktree list, conflicts --union, powershell, ndjson (#71)
17
+
6
18
  ## v0.32.0 (2026-06-20)
7
19
 
8
20
  ### BREAKING CHANGE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitwise-cli
3
- Version: 0.32.0
3
+ Version: 0.33.1
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
@@ -27,7 +27,7 @@ Classifier: Topic :: Software Development :: Version Control :: Git
27
27
  Classifier: Typing :: Typed
28
28
  Requires-Python: >=3.10
29
29
  Requires-Dist: rich-argparse<2,>=1.8.0
30
- Requires-Dist: rich<15,>=13.0
30
+ Requires-Dist: rich<16,>=13.0
31
31
  Requires-Dist: shtab<2,>=1.8.0
32
32
  Description-Content-Type: text/markdown
33
33
 
@@ -135,7 +135,7 @@ gitwise summarize
135
135
  | `gitwise status` | Enhanced status with staged/unstaged and ahead/behind |
136
136
  | `gitwise commands --json` | List subcommands with aliases and metadata |
137
137
  | `gitwise schema <command> --json` | Return versioned JSON Schema for command inputs |
138
- | `gitwise completions <shell>` | Generate shell completion scripts (bash/zsh/fish) |
138
+ | `gitwise completions <shell>` | Generate shell completion scripts (bash/zsh/fish/powershell) |
139
139
  | `gitwise pr` | List/check/view PRs via GitHub CLI |
140
140
 
141
141
  For all commands, examples, aliases, and JSON usage, see:
@@ -188,6 +188,19 @@ gitwise completions zsh > ~/.zsh/completions/_gitwise
188
188
  gitwise completions fish > ~/.config/fish/completions/gitwise.fish
189
189
  ```
190
190
 
191
+ **PowerShell** (Windows / PowerShell Core): generate and dot-source the
192
+ `Register-ArgumentCompleter` script. Add it to your `$PROFILE` for persistence:
193
+
194
+ ```powershell
195
+ gitwise completions powershell > gitwise.ps1
196
+ . .\gitwise.ps1
197
+ # or, to load on every session:
198
+ Add-Content $PROFILE ('. ' + ((Resolve-Path 'gitwise.ps1').Path))
199
+ ```
200
+
201
+ Completion covers subcommands as the first token and per-command flags
202
+ (`--json`, `--dry-run`, `--max-count`, etc.) thereafter.
203
+
191
204
  ## Demo
192
205
 
193
206
  [![asciicast](https://asciinema.org/a/6tm4TnYMygEQT7ef.svg)](https://asciinema.org/a/6tm4TnYMygEQT7ef)
@@ -102,7 +102,7 @@ gitwise summarize
102
102
  | `gitwise status` | Enhanced status with staged/unstaged and ahead/behind |
103
103
  | `gitwise commands --json` | List subcommands with aliases and metadata |
104
104
  | `gitwise schema <command> --json` | Return versioned JSON Schema for command inputs |
105
- | `gitwise completions <shell>` | Generate shell completion scripts (bash/zsh/fish) |
105
+ | `gitwise completions <shell>` | Generate shell completion scripts (bash/zsh/fish/powershell) |
106
106
  | `gitwise pr` | List/check/view PRs via GitHub CLI |
107
107
 
108
108
  For all commands, examples, aliases, and JSON usage, see:
@@ -155,6 +155,19 @@ gitwise completions zsh > ~/.zsh/completions/_gitwise
155
155
  gitwise completions fish > ~/.config/fish/completions/gitwise.fish
156
156
  ```
157
157
 
158
+ **PowerShell** (Windows / PowerShell Core): generate and dot-source the
159
+ `Register-ArgumentCompleter` script. Add it to your `$PROFILE` for persistence:
160
+
161
+ ```powershell
162
+ gitwise completions powershell > gitwise.ps1
163
+ . .\gitwise.ps1
164
+ # or, to load on every session:
165
+ Add-Content $PROFILE ('. ' + ((Resolve-Path 'gitwise.ps1').Path))
166
+ ```
167
+
168
+ Completion covers subcommands as the first token and per-command flags
169
+ (`--json`, `--dry-run`, `--max-count`, etc.) thereafter.
170
+
158
171
  ## Demo
159
172
 
160
173
  [![asciicast](https://asciinema.org/a/6tm4TnYMygEQT7ef.svg)](https://asciinema.org/a/6tm4TnYMygEQT7ef)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # gw -- short wrapper for gitwise. See bin/gitwise for Python resolution logic.
3
+ set -Eeuo pipefail
4
+ _dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ exec "$_dir/gitwise" "$@"
@@ -1,6 +1,6 @@
1
1
  """gitwise -- CLI for optimizing git workflows and coding-agent integration."""
2
2
 
3
- __version__ = "0.32.0"
3
+ __version__ = "0.33.1"
4
4
 
5
5
 
6
6
  def get_version() -> str:
@@ -1,4 +1,4 @@
1
- """Shell completions generation: bash, zsh, fish."""
1
+ """Shell completions generation: bash, zsh, fish, powershell."""
2
2
 
3
3
  import argparse
4
4
 
@@ -20,6 +20,9 @@ def build_completions_script(*, shell: str, prog: str) -> str:
20
20
  if shell == "fish":
21
21
  return _build_fish_completions_script(parser=parser, prog=prog)
22
22
 
23
+ if shell == "powershell":
24
+ return _build_powershell_completions_script(parser=parser, prog=prog)
25
+
23
26
  raise ValueError(f"unsupported shell: {shell}")
24
27
 
25
28
 
@@ -90,3 +93,75 @@ def _build_fish_completions_script(*, parser: argparse.ArgumentParser, prog: str
90
93
  lines.append(line)
91
94
 
92
95
  return "\n".join(lines) + "\n"
96
+
97
+
98
+ def _collect_flags(action: argparse.Action) -> list[str]:
99
+ """Return the long/short option strings for an argparse action."""
100
+ return [flag for flag in action.option_strings if flag.startswith("-")]
101
+
102
+
103
+ def _build_powershell_completions_script(*, parser: argparse.ArgumentParser, prog: str) -> str:
104
+ """Generate a PowerShell ``Register-ArgumentCompleter`` script from the parser tree.
105
+
106
+ Completes subcommands as the first token and per-command flags thereafter.
107
+ Hand-built because ``shtab`` has no PowerShell backend.
108
+ """
109
+ sub_action = _subparsers_action(parser)
110
+ command_names = sorted(sub_action.choices.keys()) if sub_action else []
111
+
112
+ per_command_flags: dict[str, list[str]] = {}
113
+ global_flags: list[str] = []
114
+ for action in parser._actions:
115
+ if action.dest == "help" or isinstance(action, argparse._SubParsersAction):
116
+ continue
117
+ global_flags.extend(_collect_flags(action))
118
+
119
+ if sub_action is not None:
120
+ for command, command_parser in sorted(sub_action.choices.items()):
121
+ flags: list[str] = []
122
+ for action in command_parser._actions:
123
+ if action.dest == "help" or isinstance(action, argparse._SubParsersAction):
124
+ continue
125
+ flags.extend(_collect_flags(action))
126
+ per_command_flags[command] = sorted(set(flags))
127
+
128
+ commands_ps = ",".join(f"'{c}'" for c in command_names)
129
+ global_flags_ps = ",".join(f"'{f}'" for f in sorted(set(global_flags)))
130
+ per_command_block = "\n".join(
131
+ " '{}' = @({})".format(cmd, ", ".join(f'"{f}"' for f in flags))
132
+ for cmd, flags in per_command_flags.items()
133
+ )
134
+
135
+ return f"""# PowerShell completion for {prog}
136
+ # Source it from your PowerShell profile:
137
+ # Add-Content $PROFILE ('. ' + ((Resolve-Path 'path/to/{prog}.ps1').Path))
138
+ # or dot-source: . ./{prog}.ps1
139
+ Register-ArgumentCompleter -Native -CommandName '{prog}' -ScriptBlock {{
140
+ param($wordToComplete, $commandAst, $cursorPosition)
141
+
142
+ $commands = @({commands_ps})
143
+ $globalFlags = @({global_flags_ps})
144
+ $perCommandFlags = @{{
145
+ {per_command_block}
146
+ }}
147
+
148
+ # Only tokens fully before the cursor are "completed"; the word at the
149
+ # cursor is being typed and must not be mistaken for the subcommand.
150
+ $priorTokens = @($commandAst.CommandElements | Select-Object -Skip 1 | Where-Object {{ $_.Extent.EndOffset -lt $cursorPosition }})
151
+ $subcommand = ($priorTokens | Where-Object {{ $_.Value -notlike '-*' }} | Select-Object -First 1).Value
152
+
153
+ if (-not $subcommand) {{
154
+ $candidates = $commands + $globalFlags
155
+ }}
156
+ elseif ($perCommandFlags.ContainsKey($subcommand)) {{
157
+ $candidates = $perCommandFlags[$subcommand] + $globalFlags
158
+ }}
159
+ else {{
160
+ $candidates = @()
161
+ }}
162
+
163
+ $candidates | Where-Object {{ $_ -like "$wordToComplete*" }} |
164
+ Sort-Object -Unique |
165
+ ForEach-Object {{ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }}
166
+ }}
167
+ """
@@ -206,6 +206,7 @@ def _run_diff(args: argparse.Namespace) -> int:
206
206
  summary=args.summary,
207
207
  scan_secrets=args.scan_secrets,
208
208
  as_json=args.json,
209
+ json_lines=getattr(args, "json_lines", False),
209
210
  )
210
211
 
211
212
 
@@ -215,6 +216,7 @@ def _run_log(args: argparse.Namespace) -> int:
215
216
 
216
217
  return run_log(
217
218
  as_json=args.json,
219
+ json_lines=getattr(args, "json_lines", False),
218
220
  oneline=args.oneline,
219
221
  graph=args.graph,
220
222
  author=args.author,
@@ -353,7 +355,7 @@ def _run_conflicts(args: argparse.Namespace) -> int:
353
355
  """Dispatch to ``conflicts`` subcommand."""
354
356
  from gitwise.conflicts import run_conflicts
355
357
 
356
- return run_conflicts(ours=args.ours, theirs=args.theirs, as_json=args.json)
358
+ return run_conflicts(ours=args.ours, theirs=args.theirs, union=args.union, as_json=args.json)
357
359
 
358
360
 
359
361
  def _run_suggest(args: argparse.Namespace) -> int:
@@ -163,7 +163,7 @@ def build_parser() -> argparse.ArgumentParser:
163
163
  p.add_argument("--yes", "-y", action="store_true")
164
164
 
165
165
  p = sub.add_parser("worktree", help="worktree helpers for Claude agents", parents=[parent])
166
- p.add_argument("action", choices=["new", "clean"], nargs="?", metavar="new|clean")
166
+ p.add_argument("action", choices=["new", "clean", "list"], nargs="?", metavar="new|clean|list")
167
167
  p.add_argument("branch", nargs="?")
168
168
  p.add_argument("--dry-run", action="store_true")
169
169
 
@@ -189,6 +189,12 @@ def build_parser() -> argparse.ArgumentParser:
189
189
  action="store_true",
190
190
  help="scan the diff for leaked credentials (advisory, opt-in)",
191
191
  )
192
+ p.add_argument(
193
+ "--json-lines",
194
+ action="store_true",
195
+ dest="json_lines",
196
+ help="stream one JSON envelope per changed file (NDJSON)",
197
+ )
192
198
  p.add_argument(
193
199
  "refspec",
194
200
  nargs="?",
@@ -211,6 +217,12 @@ def build_parser() -> argparse.ArgumentParser:
211
217
  p.add_argument("--until", type=str, default=None, help="show commits until date")
212
218
  p.add_argument("--file", type=str, default=None, help="show commits for file")
213
219
  p.add_argument("--max-count", type=int, default=20, dest="max_count", help="max commits")
220
+ p.add_argument(
221
+ "--json-lines",
222
+ action="store_true",
223
+ dest="json_lines",
224
+ help="stream one JSON envelope per commit (NDJSON; implies JSON mode)",
225
+ )
214
226
 
215
227
  p = sub.add_parser("show", help="commit inspector", parents=[parent])
216
228
  p.add_argument("ref", nargs="?", default="HEAD", help="commit ref (default: HEAD)")
@@ -316,8 +328,16 @@ def build_parser() -> argparse.ArgumentParser:
316
328
  p = sub.add_parser(
317
329
  "conflicts", help="conflict detection and resolution helper", parents=[parent]
318
330
  )
319
- p.add_argument("--ours", action="store_true", help="resolve all conflicts using ours")
320
- p.add_argument("--theirs", action="store_true", help="resolve all conflicts using theirs")
331
+ strategy = p.add_mutually_exclusive_group()
332
+ strategy.add_argument("--ours", action="store_true", help="resolve all conflicts using ours")
333
+ strategy.add_argument(
334
+ "--theirs", action="store_true", help="resolve all conflicts using theirs"
335
+ )
336
+ strategy.add_argument(
337
+ "--union",
338
+ action="store_true",
339
+ help="resolve all conflicts keeping both sides (union)",
340
+ )
321
341
 
322
342
  p = sub.add_parser(
323
343
  "suggest",
@@ -369,12 +389,12 @@ def build_parser() -> argparse.ArgumentParser:
369
389
 
370
390
  p = sub.add_parser(
371
391
  "completions",
372
- help="print shell completion script (bash/zsh/fish)",
392
+ help="print shell completion script (bash/zsh/fish/powershell)",
373
393
  parents=[parent],
374
394
  )
375
395
  p.add_argument(
376
396
  "shell",
377
- choices=["bash", "zsh", "fish"],
397
+ choices=["bash", "zsh", "fish", "powershell"],
378
398
  default="bash",
379
399
  nargs="?",
380
400
  help="target shell for completion script",
@@ -351,6 +351,14 @@
351
351
  "es": "{count} conflicto(s) resuelto(s) con theirs",
352
352
  "en": "{count} conflict(s) resolved with theirs"
353
353
  },
354
+ "conflicts_resolved_union": {
355
+ "es": "{count} conflicto(s) resuelto(s) con union (ambos lados)",
356
+ "en": "{count} conflict(s) resolved with union (both sides)"
357
+ },
358
+ "conflicts_union_failed": {
359
+ "es": "no se pudo resolver union para {file}",
360
+ "en": "could not union-resolve {file}"
361
+ },
354
362
  "continue_prompt": {
355
363
  "es": "¿continuar? [s/N] ",
356
364
  "en": "continue? [y/N] "
@@ -2023,6 +2031,22 @@
2023
2031
  "es": " rama: {branch}",
2024
2032
  "en": " branch: {branch}"
2025
2033
  },
2034
+ "worktree_flag_locked": {
2035
+ "es": "bloqueado",
2036
+ "en": "locked"
2037
+ },
2038
+ "worktree_flag_prunable": {
2039
+ "es": "podable",
2040
+ "en": "prunable"
2041
+ },
2042
+ "worktree_list_empty": {
2043
+ "es": "no hay worktrees registrados",
2044
+ "en": "no registered worktrees"
2045
+ },
2046
+ "worktree_list_header": {
2047
+ "es": "worktrees ({count}):",
2048
+ "en": "worktrees ({count}):"
2049
+ },
2026
2050
  "worktree_created": {
2027
2051
  "es": "worktree creado: {path}",
2028
2052
  "en": "worktree created: {path}"
@@ -1,5 +1,7 @@
1
1
  """gitwise conflicts — conflict detection and resolution helper."""
2
2
 
3
+ import subprocess
4
+ import tempfile
3
5
  from pathlib import Path
4
6
 
5
7
  from gitwise.git import require_root
@@ -19,6 +21,17 @@ from gitwise.utils.json_envelope import error_envelope, ok_envelope
19
21
  from gitwise.utils.parsing import stripped_non_empty_lines, to_int
20
22
 
21
23
 
24
+ def _git_bytes(args: list[str], *, cwd: Path) -> tuple[int, bytes, bytes]:
25
+ """Run git capturing raw bytes (no text decoding) for binary-safe I/O."""
26
+ r = subprocess.run(
27
+ ["git", *args],
28
+ cwd=cwd,
29
+ capture_output=True,
30
+ timeout=120,
31
+ )
32
+ return r.returncode, r.stdout, r.stderr
33
+
34
+
22
35
  def _find_conflict_files(root: Path) -> list[str]:
23
36
  """Return file paths with unmerged (U) status."""
24
37
  r = git_run(["diff", "--name-only", "--diff-filter=U"], cwd=root, check=False)
@@ -40,7 +53,9 @@ def _conflict_markers(root: Path, filepath: str) -> int:
40
53
 
41
54
 
42
55
  def _resolve_all_conflicts(*, root: Path, conflicts: list[str], strategy: str) -> int:
43
- """Resolve conflicts in all files using ``--ours`` or ``--theirs``, then stage."""
56
+ """Resolve conflicts in all files using ``--ours``/``--theirs``, then stage."""
57
+ if strategy == "union":
58
+ return _resolve_union(root=root, conflicts=conflicts)
44
59
  checkout_flag = "--ours" if strategy == "ours" else "--theirs"
45
60
  result = git_run(["checkout", checkout_flag, "--"] + conflicts, cwd=root, check=False)
46
61
  if result.returncode != 0:
@@ -50,6 +65,58 @@ def _resolve_all_conflicts(*, root: Path, conflicts: list[str], strategy: str) -
50
65
  return 0
51
66
 
52
67
 
68
+ def _stage_blob(root: Path, stage_ref: str) -> bytes:
69
+ """Return the raw bytes of a staged blob (e.g. ``:2:path`` = ours).
70
+
71
+ Binary-safe (no UTF-8 round-trip). Returns b"" only for a missing base
72
+ (``:1:``); callers treat base absence as an empty common ancestor.
73
+ """
74
+ rc, out, _ = _git_bytes(["show", stage_ref], cwd=root)
75
+ return out if rc == 0 else b""
76
+
77
+
78
+ def _resolve_union(*, root: Path, conflicts: list[str]) -> int:
79
+ """Resolve conflicts with ``git merge-file --union`` (keeps both sides, no markers).
80
+
81
+ For each file, materialize the base (:1:), ours (:2:), theirs (:3:) stage
82
+ blobs as raw bytes, union-merge them, write the result back, and stage it.
83
+ Uses bytes throughout so binary/non-UTF-8 conflict files keep full fidelity.
84
+ """
85
+ for filepath in conflicts:
86
+ ours = _stage_blob(root, f":2:{filepath}")
87
+ base = _stage_blob(root, f":1:{filepath}")
88
+ theirs = _stage_blob(root, f":3:{filepath}")
89
+ if not ours and not theirs:
90
+ error(t("conflicts_union_failed", file=filepath))
91
+ return 1
92
+ with tempfile.TemporaryDirectory() as tmp:
93
+ ours_p = Path(tmp) / "ours"
94
+ base_p = Path(tmp) / "base"
95
+ theirs_p = Path(tmp) / "theirs"
96
+ ours_p.write_bytes(ours)
97
+ base_p.write_bytes(base)
98
+ theirs_p.write_bytes(theirs)
99
+ rc, out, err = _git_bytes(
100
+ ["merge-file", "--union", "-p", str(ours_p), str(base_p), str(theirs_p)],
101
+ cwd=root,
102
+ )
103
+ if rc not in (0, 1):
104
+ error(
105
+ err.decode("utf-8", errors="replace").strip()
106
+ or t("conflicts_union_failed", file=filepath)
107
+ )
108
+ return 1
109
+ (root / filepath).write_bytes(out)
110
+ add_rc, _, add_err = _git_bytes(["add", "--", filepath], cwd=root)
111
+ if add_rc != 0:
112
+ error(
113
+ add_err.decode("utf-8", errors="replace").strip()
114
+ or t("conflicts_union_failed", file=filepath)
115
+ )
116
+ return 1
117
+ return 0
118
+
119
+
53
120
  def _conflict_details(root: Path, conflicts: list[str]) -> list[dict[str, str | int]]:
54
121
  """Return ``[{file, markers}]`` with conflict marker counts per file."""
55
122
  details: list[dict[str, str | int]] = []
@@ -76,7 +143,11 @@ def _resolve_by_strategy(*, root: Path, conflicts: list[str], strategy: str, as_
76
143
  if as_json:
77
144
  print_json(ok_envelope("conflicts", resolved=len(conflicts), strategy=strategy))
78
145
  return 0
79
- key = "conflicts_resolved_ours" if strategy == "ours" else "conflicts_resolved_theirs"
146
+ key = {
147
+ "ours": "conflicts_resolved_ours",
148
+ "theirs": "conflicts_resolved_theirs",
149
+ "union": "conflicts_resolved_union",
150
+ }[strategy]
80
151
  ok(t(key, count=str(len(conflicts))))
81
152
  return 0
82
153
 
@@ -100,6 +171,7 @@ def run_conflicts(
100
171
  *,
101
172
  ours: bool = False,
102
173
  theirs: bool = False,
174
+ union: bool = False,
103
175
  as_json: bool = False,
104
176
  ) -> int:
105
177
  """Entry point for the ``gitwise conflicts`` command."""
@@ -115,6 +187,11 @@ def run_conflicts(
115
187
  if not conflicts:
116
188
  return _report_no_conflicts(as_json=as_json)
117
189
 
190
+ if union:
191
+ return _resolve_by_strategy(
192
+ root=root, conflicts=conflicts, strategy="union", as_json=as_json
193
+ )
194
+
118
195
  if ours:
119
196
  return _resolve_by_strategy(
120
197
  root=root, conflicts=conflicts, strategy="ours", as_json=as_json
@@ -122,6 +122,19 @@ def _name_status_details(
122
122
  return _parse_name_status_lines(r.stdout.splitlines())
123
123
 
124
124
 
125
+ def _normalize_rename_path(path: str) -> str:
126
+ """Collapse git's ``{old => new}`` rename syntax to the destination path."""
127
+ if " => " not in path or "{" not in path or "}" not in path:
128
+ return path
129
+ # forms: "{old => new}", "dir/{old => new}", "{a => b}/c"
130
+ prefix, _, rest = path.partition("{")
131
+ inner, _, suffix = rest.partition("}")
132
+ if " => " in inner:
133
+ _, _, new = inner.partition(" => ")
134
+ return f"{prefix}{new}{suffix}"
135
+ return path
136
+
137
+
125
138
  def _numstat_details(
126
139
  cwd: Path, *, staged: bool, refspec: str | None, paths: list[str] | None
127
140
  ) -> dict[str, DiffFileEntry]:
@@ -147,7 +160,7 @@ def _numstat_details(
147
160
  continue
148
161
  add_raw = parts[0].strip()
149
162
  del_raw = parts[1].strip()
150
- path = parts[2].strip()
163
+ path = _normalize_rename_path(parts[2].strip())
151
164
  if not path:
152
165
  continue
153
166
 
@@ -485,6 +498,7 @@ def run_diff(
485
498
  summary: bool = False,
486
499
  scan_secrets: bool = False,
487
500
  as_json: bool = False,
501
+ json_lines: bool = False,
488
502
  ) -> int:
489
503
  """Render a diff of changed files, a refspec, or a range.
490
504
 
@@ -502,12 +516,32 @@ def run_diff(
502
516
  cwd = root
503
517
 
504
518
  if not refspec and not staged and not _has_commits(cwd):
505
- if as_json:
506
- print_json(ok_envelope("diff", files=[], count=0, note=t("no_commits_yet")))
519
+ if as_json or json_lines:
520
+ note_env = ok_envelope("diff", files=[], count=0, note=t("no_commits_yet"))
521
+ if json_lines:
522
+ from gitwise.output import print_json_line
523
+
524
+ print_json_line(note_env)
525
+ else:
526
+ print_json(note_env)
507
527
  return 0
508
528
  info(t("no_commits_yet"))
509
529
  return 0
510
530
 
531
+ if json_lines:
532
+ from gitwise.output import print_json_line
533
+
534
+ numstat_details = _numstat_details(cwd, staged=staged, refspec=refspec, paths=paths)
535
+ status_details = _name_status_details(cwd, staged=staged, refspec=refspec, paths=paths)
536
+ for path, entry in numstat_details.items():
537
+ st = status_details.get(path, {})
538
+ code = str(st.get("code") or st.get("status") or "")
539
+ if code:
540
+ entry["code"] = code
541
+ entry["status_label"] = status_label(code)
542
+ print_json_line(ok_envelope("diff", file=entry))
543
+ return 0
544
+
511
545
  if scan_secrets:
512
546
  return _render_secret_scan_output(root=cwd, refspec=refspec, paths=paths, as_json=as_json)
513
547
 
@@ -195,8 +195,22 @@ def _run_log_json(
195
195
  until: str | None,
196
196
  file: str | None,
197
197
  max_count: int,
198
+ json_lines: bool = False,
198
199
  ) -> int:
199
- """Execute git log and emit structured JSON with enriched per-commit stats."""
200
+ """Execute git log and emit structured JSON with enriched per-commit stats.
201
+
202
+ With ``json_lines``, stream one compact v3 envelope per commit (NDJSON) for
203
+ incremental processing instead of a single blob. All output paths (including
204
+ errors) emit single-line envelopes to preserve the NDJSON record contract.
205
+ """
206
+ from gitwise.output import print_json_line
207
+
208
+ def _emit(env: dict) -> None:
209
+ if json_lines:
210
+ print_json_line(env)
211
+ else:
212
+ print_json(env)
213
+
200
214
  try:
201
215
  args = _build_log_json_args(
202
216
  author=author,
@@ -207,14 +221,14 @@ def _run_log_json(
207
221
  max_count=max_count + 1,
208
222
  )
209
223
  except ValueError as exc:
210
- print_json(error_envelope("log", error=str(exc), code="invalid_argument"))
224
+ _emit(error_envelope("log", error=str(exc), code="invalid_argument"))
211
225
  return 1
212
226
  result = git_run(args, cwd=root, check=False)
213
227
  if result.returncode != 0:
214
228
  if "does not have any commits yet" in result.stderr:
215
- print_json(ok_envelope("log", commits=[], count=0, total=0, truncated=False))
229
+ _emit(ok_envelope("log", commits=[], count=0, total=0, truncated=False))
216
230
  return 0
217
- print_json(
231
+ _emit(
218
232
  error_envelope(
219
233
  "log",
220
234
  error=t("git_log_failed", error=result.stderr.strip()),
@@ -226,6 +240,12 @@ def _run_log_json(
226
240
  total = len(commits)
227
241
  truncated = total > max_count
228
242
  commits = commits[:max_count]
243
+ if json_lines:
244
+ from gitwise.output import print_json_line
245
+
246
+ for commit in commits:
247
+ print_json_line(ok_envelope("log", commit=commit))
248
+ return 0
229
249
  hints: list[str] = []
230
250
  if truncated:
231
251
  hints.append(f"gitwise log --max-count {max_count} (more commits exist)")
@@ -324,6 +344,7 @@ def _run_log_human_plain_or_graph(*, raw: str, graph: bool, oneline: bool) -> bo
324
344
  def run_log(
325
345
  *,
326
346
  as_json: bool = False,
347
+ json_lines: bool = False,
327
348
  oneline: bool = False,
328
349
  graph: bool = False,
329
350
  author: str | None = None,
@@ -340,7 +361,7 @@ def run_log(
340
361
  if root is None:
341
362
  return 1
342
363
 
343
- if as_json:
364
+ if as_json or json_lines:
344
365
  return _run_log_json(
345
366
  root=root,
346
367
  author=author,
@@ -349,6 +370,7 @@ def run_log(
349
370
  until=until,
350
371
  file=file,
351
372
  max_count=max_count,
373
+ json_lines=json_lines,
352
374
  )
353
375
 
354
376
  return _run_log_human(
@@ -279,6 +279,14 @@ def print_json(data: Any) -> None:
279
279
  print(json.dumps(data, ensure_ascii=False, separators=(",", ":")))
280
280
 
281
281
 
282
+ def print_json_line(data: Any) -> None:
283
+ """Print data as a single compact JSON line (NDJSON), ignoring pretty mode.
284
+
285
+ Used by ``--json-lines`` streaming so each record is one parseable line.
286
+ """
287
+ print(json.dumps(data, ensure_ascii=False, separators=(",", ":")))
288
+
289
+
282
290
  def print_blank() -> None:
283
291
  """Print an empty line to stdout."""
284
292
  if _should_use_rich():
@@ -38,7 +38,7 @@ def _list_worktrees(cwd: Path) -> list[dict]:
38
38
  }
39
39
  elif line.startswith("branch refs/heads/"):
40
40
  current["branch"] = line.removeprefix("branch refs/heads/")
41
- elif line == "locked":
41
+ elif line.startswith("locked"):
42
42
  current["locked"] = True
43
43
  elif line.startswith("prunable"):
44
44
  current["prunable"] = True
@@ -107,6 +107,28 @@ def _worktree_new_json(branch: str, root: Path) -> tuple[int, dict]:
107
107
  return rc, data
108
108
 
109
109
 
110
+ def _worktree_list(cwd: Path, *, as_json: bool = False) -> int:
111
+ """List registered worktrees (human table or v3 JSON envelope)."""
112
+ worktrees = _list_worktrees(cwd)
113
+ if as_json:
114
+ print_json(ok_envelope("worktree", worktrees=worktrees, count=len(worktrees)))
115
+ return 0
116
+ if not worktrees:
117
+ info(t("worktree_list_empty"))
118
+ return 0
119
+ print_header(t("worktree_list_header", count=str(len(worktrees))))
120
+ for wt in worktrees:
121
+ branch = wt["branch"] or t("unknown_branch")
122
+ flags = []
123
+ if wt["locked"]:
124
+ flags.append(t("worktree_flag_locked"))
125
+ if wt["prunable"]:
126
+ flags.append(t("worktree_flag_prunable"))
127
+ suffix = f" [{', '.join(flags)}]" if flags else ""
128
+ print_bracket(wt["path"], t("branch_label", branch=branch) + suffix)
129
+ return 0
130
+
131
+
110
132
  def _worktree_clean(cwd: Path, *, dry_run: bool = False, as_json: bool = False) -> int:
111
133
  """Prune stale worktree admin files and report orphaned worktrees."""
112
134
  # git worktree prune removes stale admin files
@@ -193,6 +215,9 @@ def run_worktree(
193
215
  elif action == "clean":
194
216
  return _worktree_clean(root, dry_run=dry_run, as_json=as_json)
195
217
 
218
+ elif action == "list":
219
+ return _worktree_list(root, as_json=as_json)
220
+
196
221
  else:
197
222
  error(t("worktree_usage_full"))
198
223
  return 1