ace-git-copilot 0.3.0__tar.gz → 0.3.2__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 (122) hide show
  1. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/PKG-INFO +30 -1
  2. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/README.md +29 -0
  3. ace_git_copilot-0.3.2/ace/__init__.py +1 -0
  4. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/changelog_generator.py +18 -0
  5. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/commit_generator.py +7 -2
  6. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/llm_factory.py +7 -6
  7. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/cli.py +34 -13
  8. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/dashboard.py +12 -10
  9. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/display.py +61 -2
  10. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/prompts.py +3 -2
  11. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/pyproject.toml +1 -1
  12. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/conftest.py +63 -34
  13. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/test_tier1_features.py +18 -6
  14. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/test_tier2_boundaries.py +12 -7
  15. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/test_tier3_combinations.py +9 -3
  16. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/test_tier4_workloads.py +11 -5
  17. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_help.py +2 -2
  18. ace_git_copilot-0.3.0/ace/__init__.py +0 -1
  19. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/AGENTS.md +0 -0
  20. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/BRIEFING.md +0 -0
  21. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/ORIGINAL_REQUEST.md +0 -0
  22. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/e2e_testing_track/BRIEFING.md +0 -0
  23. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/e2e_testing_track/ORIGINAL_REQUEST.md +0 -0
  24. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/e2e_testing_track/SCOPE.md +0 -0
  25. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/e2e_testing_track/progress.md +0 -0
  26. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/BRIEFING.md +0 -0
  27. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/ORIGINAL_REQUEST.md +0 -0
  28. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/emojis_list.txt +0 -0
  29. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/find_unused_modules.py +0 -0
  30. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/handoff.md +0 -0
  31. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/measure_lazy_startup.py +0 -0
  32. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/measure_startup.py +0 -0
  33. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/profile_imports.py +0 -0
  34. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/progress.md +0 -0
  35. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/run_importtime.py +0 -0
  36. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/search_banner.py +0 -0
  37. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/search_emojis.py +0 -0
  38. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/search_git_usages.py +0 -0
  39. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/search_usages.py +0 -0
  40. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/test_import_profiler.py +0 -0
  41. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/test_mocked_sys.py +0 -0
  42. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/handoff.md +0 -0
  43. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/implementation_track/BRIEFING.md +0 -0
  44. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/implementation_track/ORIGINAL_REQUEST.md +0 -0
  45. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/implementation_track/explorer_initial_report.md +0 -0
  46. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/implementation_track/progress.md +0 -0
  47. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/.gitkeep +0 -0
  48. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/BRIEFING.md +0 -0
  49. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/ORIGINAL_REQUEST.md +0 -0
  50. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/PROJECT.md +0 -0
  51. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/progress.md +0 -0
  52. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/teamwork_preview_explorer_e2e_explore/BRIEFING.md +0 -0
  53. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/teamwork_preview_explorer_e2e_explore/ORIGINAL_REQUEST.md +0 -0
  54. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/teamwork_preview_explorer_e2e_explore/handoff.md +0 -0
  55. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/teamwork_preview_explorer_e2e_explore/progress.md +0 -0
  56. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_e2e_testing/BRIEFING.md +0 -0
  57. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_e2e_testing/ORIGINAL_REQUEST.md +0 -0
  58. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_e2e_testing/progress.md +0 -0
  59. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_m1_startup/BRIEFING.md +0 -0
  60. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_m1_startup/ORIGINAL_REQUEST.md +0 -0
  61. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_m1_startup/progress.md +0 -0
  62. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.env.example +0 -0
  63. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.github/workflows/tests.yml +0 -0
  64. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.gitignore +0 -0
  65. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/CODE_OF_CONDUCT.md +0 -0
  66. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/CONTRIBUTING.md +0 -0
  67. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/LICENSE +0 -0
  68. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/PROJECT.md +0 -0
  69. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/SECURITY.md +0 -0
  70. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/SUPPORT.md +0 -0
  71. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/TEST_INFRA.md +0 -0
  72. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/TEST_READY.md +0 -0
  73. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/__main__.py +0 -0
  74. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/code_reviewer.py +0 -0
  75. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/conflict_resolver.py +0 -0
  76. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/gitignore_generator.py +0 -0
  77. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/history_analyzer.py +0 -0
  78. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/intent_parser.py +0 -0
  79. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/pr_drafter.py +0 -0
  80. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/changelog.py +0 -0
  81. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/commit.py +0 -0
  82. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/conflict.py +0 -0
  83. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/doctor.py +0 -0
  84. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/explain.py +0 -0
  85. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/ignore.py +0 -0
  86. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/intent.py +0 -0
  87. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/pr.py +0 -0
  88. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/rebase.py +0 -0
  89. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/review.py +0 -0
  90. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/search.py +0 -0
  91. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/undo.py +0 -0
  92. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/rebase_helper.py +0 -0
  93. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/config.py +0 -0
  94. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/context.py +0 -0
  95. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/diagnostics.py +0 -0
  96. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/git_ops.py +0 -0
  97. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/hooks.py +0 -0
  98. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/safety.py +0 -0
  99. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/banner.py +0 -0
  100. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/themes.py +0 -0
  101. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/utils/conflict_parser.py +0 -0
  102. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/utils/diff_parser.py +0 -0
  103. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/utils/json_utils.py +0 -0
  104. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/importtime.txt +0 -0
  105. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/importtime_optimized.txt +0 -0
  106. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/conftest.py +0 -0
  107. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_changelog_generator.py +0 -0
  108. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_code_reviewer.py +0 -0
  109. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_conflict_resolver.py +0 -0
  110. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_diagnostics.py +0 -0
  111. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_diff_trimmer.py +0 -0
  112. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_git_ops.py +0 -0
  113. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_history_analyzer.py +0 -0
  114. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_hooks.py +0 -0
  115. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_ignore.py +0 -0
  116. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_intent_parser.py +0 -0
  117. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_llm_factory.py +0 -0
  118. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_pr_drafter.py +0 -0
  119. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_rebase_helper.py +0 -0
  120. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_safety.py +0 -0
  121. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_search.py +0 -0
  122. {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_undo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ace-git-copilot
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: AI-powered Git copilot — talk to Git in plain English
5
5
  Project-URL: Homepage, https://github.com/jachinsamuel/Ace
6
6
  Project-URL: Documentation, https://github.com/jachinsamuel/Ace#readme
@@ -193,3 +193,32 @@ The dashboard features:
193
193
  ## License
194
194
 
195
195
  Distributed under the MIT License. See [LICENSE](LICENSE) for more details.
196
+
197
+ ---
198
+
199
+ ## Changelog
200
+
201
+ ### v0.3.1 — Patch (2026-06-30)
202
+ * Fixed invisible key labels (`[c]`, `[r]`, etc.) in the dashboard and search menus caused by Rich markup tag conflicts.
203
+ * Fixed `[Y/n]` confirmation prompt rendering invisibly.
204
+
205
+ ### v0.3.0 — UI Polish (2026-06-30)
206
+ * Rewrote all UI output modules (`display.py`, `prompts.py`, `dashboard.py`, `themes.py`) for a polished, professional look.
207
+ * Removed all unprofessional emojis; replaced with clean ASCII status symbols (`>>`, `**`, `!!`, `EE`).
208
+ * Dashboard panels now use consistent `ROUNDED` borders, colour-coded file lists (`+` staged, `~` unstaged, `?` untracked), and a styled commit history table.
209
+ * Execution plan table uses `SIMPLE_HEAD` style with colour-highlighted `git` and `ace` command prefixes.
210
+ * Commit message panel shows character count badge coloured green/amber/red relative to the 72-char limit.
211
+ * AI code review score rendered as a styled badge based on score range.
212
+ * Added `[tool.ruff]` configuration to exclude `.agents/` scratch folder from lint checks.
213
+
214
+ ### v0.2.9 — Metadata & Build Fix (2026-06-30)
215
+ * Fixed broken `pyproject.toml` TOML syntax (invalid inline `urls` table).
216
+ * Added full `authors` and `[project.urls]` metadata visible on PyPI.
217
+
218
+ ### v0.2.6 — Startup Optimisation (2026-06-21)
219
+ * Implemented lazy-loading for all heavy LangChain imports — CLI startup under 200 ms.
220
+ * Added `ace switch` command for switching between sibling repositories from the dashboard.
221
+
222
+ ### v0.2.3 — Feature Expansion (2026-06-09)
223
+ * Added `ace doctor`, `ace hook`, and `ace squash` commands.
224
+ * Comprehensive E2E test suite added under `tests/e2e/`.
@@ -160,3 +160,32 @@ The dashboard features:
160
160
  ## License
161
161
 
162
162
  Distributed under the MIT License. See [LICENSE](LICENSE) for more details.
163
+
164
+ ---
165
+
166
+ ## Changelog
167
+
168
+ ### v0.3.1 — Patch (2026-06-30)
169
+ * Fixed invisible key labels (`[c]`, `[r]`, etc.) in the dashboard and search menus caused by Rich markup tag conflicts.
170
+ * Fixed `[Y/n]` confirmation prompt rendering invisibly.
171
+
172
+ ### v0.3.0 — UI Polish (2026-06-30)
173
+ * Rewrote all UI output modules (`display.py`, `prompts.py`, `dashboard.py`, `themes.py`) for a polished, professional look.
174
+ * Removed all unprofessional emojis; replaced with clean ASCII status symbols (`>>`, `**`, `!!`, `EE`).
175
+ * Dashboard panels now use consistent `ROUNDED` borders, colour-coded file lists (`+` staged, `~` unstaged, `?` untracked), and a styled commit history table.
176
+ * Execution plan table uses `SIMPLE_HEAD` style with colour-highlighted `git` and `ace` command prefixes.
177
+ * Commit message panel shows character count badge coloured green/amber/red relative to the 72-char limit.
178
+ * AI code review score rendered as a styled badge based on score range.
179
+ * Added `[tool.ruff]` configuration to exclude `.agents/` scratch folder from lint checks.
180
+
181
+ ### v0.2.9 — Metadata & Build Fix (2026-06-30)
182
+ * Fixed broken `pyproject.toml` TOML syntax (invalid inline `urls` table).
183
+ * Added full `authors` and `[project.urls]` metadata visible on PyPI.
184
+
185
+ ### v0.2.6 — Startup Optimisation (2026-06-21)
186
+ * Implemented lazy-loading for all heavy LangChain imports — CLI startup under 200 ms.
187
+ * Added `ace switch` command for switching between sibling repositories from the dashboard.
188
+
189
+ ### v0.2.3 — Feature Expansion (2026-06-09)
190
+ * Added `ace doctor`, `ace hook`, and `ace squash` commands.
191
+ * Comprehensive E2E test suite added under `tests/e2e/`.
@@ -0,0 +1 @@
1
+ __version__ = "0.3.2"
@@ -18,6 +18,24 @@ class ChangelogGenerator:
18
18
  If from_ref is None, attempts to find the latest tag.
19
19
  If no latest tag exists, falls back to the last 30 commits.
20
20
  """
21
+ if from_ref:
22
+ try:
23
+ self.git_ops.execute(f"rev-parse --verify {from_ref}")
24
+ except Exception:
25
+ raise ChangelogGeneratorError(f"Invalid starting revision: {from_ref}")
26
+
27
+ if to_ref:
28
+ try:
29
+ self.git_ops.execute(f"rev-parse --verify {to_ref}")
30
+ except Exception:
31
+ raise ChangelogGeneratorError(f"Invalid ending revision: {to_ref}")
32
+
33
+ # Check if HEAD exists first
34
+ try:
35
+ self.git_ops.execute("rev-parse --verify HEAD")
36
+ except Exception:
37
+ return ""
38
+
21
39
  to_revision = to_ref or "HEAD"
22
40
  from_revision = from_ref
23
41
 
@@ -29,8 +29,13 @@ class CommitGenerator:
29
29
  raise NoStagedChangesError("No changes are staged for commit. Stage files first using 'git add'.")
30
30
 
31
31
  staged_diff = self.git_ops.get_staged_diff()
32
- if not staged_diff.strip():
33
- # Sometimes status has staged but diff is empty (e.g. only file permissions or empty files)
32
+ has_content_changes = False
33
+ for line in staged_diff.splitlines():
34
+ if (line.startswith("+") and not line.startswith("+++")) or (line.startswith("-") and not line.startswith("---")):
35
+ has_content_changes = True
36
+ break
37
+
38
+ if not has_content_changes:
34
39
  raise NoStagedChangesError("Staged diff is empty. Cannot generate commit message.")
35
40
 
36
41
  # Format context
@@ -43,10 +43,11 @@ def ensure_ollama_model(base_url: str, model_name: str) -> None:
43
43
  return
44
44
 
45
45
  # 2. Prompt user and pull model
46
- from ace.ui.display import console, spinner
46
+ from ace.ui.display import console, spinner, print_warning, print_success, print_error, print_info
47
47
  from ace.ui.prompts import confirm
48
48
 
49
- console.print(f"\n[warning]⚠️ Ollama model [bold]{model_name}[/bold] is not downloaded locally.[/warning]")
49
+ console.print()
50
+ print_warning(f"Ollama model '{model_name}' is not downloaded locally.")
50
51
  if confirm(f"Would you like Ace to automatically pull '{model_name}' from the Ollama registry?", default=True):
51
52
  try:
52
53
  url = f"{base_url.rstrip('/')}/api/pull"
@@ -58,12 +59,12 @@ def ensure_ollama_model(base_url: str, model_name: str) -> None:
58
59
  with urllib.request.urlopen(req) as response:
59
60
  res_data = json.loads(response.read().decode("utf-8"))
60
61
  if res_data.get("status") == "success" or "success" in str(res_data):
61
- console.print(f"[success]✅ Successfully downloaded '{model_name}'![/success]\n")
62
+ print_success(f"Successfully downloaded '{model_name}'!\n")
62
63
  else:
63
- console.print(f"[info]Ollama response: {res_data}[/info]\n")
64
+ print_info(f"Ollama response: {res_data}\n")
64
65
  except Exception as e:
65
- console.print(f"[error]❌ Failed to pull model: {e}[/error]")
66
- console.print(f"[info]Please run 'ollama pull {model_name}' manually in your shell.[/info]\n")
66
+ print_error(f"Failed to pull model: {e}")
67
+ print_info(f"Please run 'ollama pull {model_name}' manually in your shell.\n")
67
68
 
68
69
  def get_llm(offline_override: bool = False) -> BaseChatModel:
69
70
  """
@@ -138,9 +138,9 @@ def main(
138
138
  r_level, r_desc, alt = SafetyChecker.analyze_command(cmd)
139
139
  if r_level == "destructive":
140
140
  highest_risk = "destructive"
141
- risk_details.append(f"[bold red]Command:[/bold] {cmd}\n[bold red]Risk:[/bold] {r_desc}")
141
+ risk_details.append(f"[bold red]Command:[/] {cmd}\n[bold red]Risk:[/] {r_desc}")
142
142
  if alt:
143
- safer_alts.append(f"[bold green]Safer Alternative:[/bold] {alt}")
143
+ safer_alts.append(f"[bold green]Safer Alternative:[/] {alt}")
144
144
  elif r_level == "moderate" and highest_risk != "destructive":
145
145
  highest_risk = "moderate"
146
146
 
@@ -191,8 +191,19 @@ def main(
191
191
  raise typer.Exit(code=1)
192
192
  else:
193
193
  try:
194
- res = git_ops.execute(cmd)
195
- outputs.append(res)
194
+ import subprocess
195
+ import sys
196
+ args = cmd.split()[1:]
197
+ res_proc = subprocess.run(
198
+ [sys.executable, "-c", "from ace.cli import app; app()"] + args,
199
+ stdout=subprocess.PIPE,
200
+ stderr=subprocess.PIPE,
201
+ text=True,
202
+ encoding="utf-8"
203
+ )
204
+ if res_proc.returncode != 0:
205
+ raise Exception(res_proc.stderr or res_proc.stdout)
206
+ outputs.append(res_proc.stdout)
196
207
  except Exception as e:
197
208
  show_error_panel(f"Failed to execute command '{cmd}': {e}", "Execution Error")
198
209
  raise typer.Exit(code=1)
@@ -207,7 +218,7 @@ def main(
207
218
 
208
219
  # Summarization flow for read-only history queries
209
220
  combined_output = "\n".join(outputs)
210
- if highest_risk == "safe" and combined_output.strip():
221
+ if highest_risk == "safe" and combined_output.strip() and not any(c.startswith("ace ") for c in commands):
211
222
  from ace.ai.history_analyzer import HistoryAnalyzer
212
223
  from rich.markdown import Markdown
213
224
  analyzer = HistoryAnalyzer(git_ops)
@@ -270,6 +281,8 @@ def commit_cmd(
270
281
  with open(prepare, "w", encoding="utf-8") as f:
271
282
  f.write(msg)
272
283
  raise typer.Exit(code=0)
284
+ except (typer.Exit, typer.Abort):
285
+ raise
273
286
  except NoStagedChangesError:
274
287
  raise typer.Exit(code=0)
275
288
  except Exception as e:
@@ -785,7 +798,8 @@ def changelog_cmd(
785
798
  show_error_panel(f"{str(e)}\n\nRun [bold]ace setup[/bold] to configure your AI credentials.", "Configuration Error")
786
799
  raise typer.Exit(code=1)
787
800
  except Exception as e:
788
- show_error_panel(f"Failed to generate changelog: {e}", "AI Error")
801
+ title = "Git Error" if "ChangelogGeneratorError" in str(type(e)) or "Cmd('git')" in str(e) or "Invalid starting revision" in str(e) or "Invalid ending revision" in str(e) else "AI Error"
802
+ show_error_panel(f"Failed to generate changelog: {e}", title)
789
803
  raise typer.Exit(code=1)
790
804
 
791
805
  # Show or write to file
@@ -1154,9 +1168,9 @@ def undo_cmd(
1154
1168
  r_level, r_desc, alt = SafetyChecker.analyze_command(cmd)
1155
1169
  if r_level == "destructive":
1156
1170
  highest_risk = "destructive"
1157
- risk_details.append(f"[bold red]Command:[/bold] {cmd}\n[bold red]Risk:[/bold] {r_desc}")
1171
+ risk_details.append(f"[bold red]Command:[/] {cmd}\n[bold red]Risk:[/] {r_desc}")
1158
1172
  if alt:
1159
- safer_alts.append(f"[bold green]Safer Alternative:[/bold] {alt}")
1173
+ safer_alts.append(f"[bold green]Safer Alternative:[/] {alt}")
1160
1174
  elif r_level == "moderate" and highest_risk != "destructive":
1161
1175
  highest_risk = "moderate"
1162
1176
 
@@ -1251,7 +1265,8 @@ def pr_cmd(
1251
1265
  with spinner(f"Generating PR description against base branch '{base}'..."):
1252
1266
  pr_data = drafter.draft_pr(base, offline=offline)
1253
1267
  except Exception as e:
1254
- show_error_panel(f"Failed to generate PR description: {e}", "AI Error")
1268
+ title = "Git Error" if "Cmd('git')" in str(e) or "git log" in str(e) or "exit code" in str(e) else "AI Error"
1269
+ show_error_panel(f"Failed to generate PR description: {e}", title)
1255
1270
  raise typer.Exit(code=1)
1256
1271
 
1257
1272
  title = pr_data.get("title", "Pull Request")
@@ -1335,10 +1350,16 @@ def search_cmd(
1335
1350
 
1336
1351
  # Show options
1337
1352
  console.print("\n[bold]Select action:[/bold]")
1338
- console.print(" [bold cyan][d][/bold cyan] -> View diff of this commit")
1339
- console.print(" [bold cyan][c][/bold cyan] -> Checkout this commit (detached HEAD)")
1340
- console.print(" [bold cyan][b][/bold cyan] -> Create and switch to new branch at this commit")
1341
- console.print(" [bold cyan][s][/bold cyan] -> Skip/Quit")
1353
+ def _key(k: str, desc: str) -> None:
1354
+ from rich.text import Text
1355
+ console.print(Text.assemble(
1356
+ (" ", ""), (f"[{k}]", "bold #00D5FF"), (f" {desc}", "#BDBDBD")
1357
+ ))
1358
+ _key("d", "View diff of this commit")
1359
+ _key("c", "Checkout this commit (detached HEAD)")
1360
+ _key("b", "Create and switch to a new branch here")
1361
+ _key("s", "Skip / Quit")
1362
+
1342
1363
 
1343
1364
  action = click.getchar().lower().strip()
1344
1365
  console.print(action)
@@ -199,16 +199,18 @@ def show_dashboard(git_ops: GitOps, offline: bool = False):
199
199
  console.print()
200
200
 
201
201
  # ── Action menu ─────────────────────────────────────────────────────
202
- menu = Table(show_header=False, box=None, padding=(0, 3), expand=False)
203
- menu.add_column(style="bold #00D5FF", justify="right", width=4)
204
- menu.add_column(style="#BDBDBD", width=20)
205
- menu.add_column(style="bold #00D5FF", justify="right", width=4)
206
- menu.add_column(style="#BDBDBD")
207
-
208
- menu.add_row("[c]", "AI Commit", "[r]", "AI Code Review")
209
- menu.add_row("[u]", "Smart Undo", "[p]", "Plan Command (AI)")
210
- menu.add_row("[s]", "Repo Stats", "[w]", "Switch Repo")
211
- menu.add_row("[q]", "Quit", "", "")
202
+ menu = Table(show_header=False, box=None, padding=(0, 4), expand=False)
203
+ menu.add_column(min_width=22)
204
+ menu.add_column(min_width=22)
205
+
206
+
207
+ def _row(key: str, label: str) -> Text:
208
+ return Text.assemble((f"[{key}]", "bold #00D5FF"), (f" {label}", "#BDBDBD"))
209
+
210
+ menu.add_row(_row("c", "AI Commit"), _row("r", "AI Code Review"))
211
+ menu.add_row(_row("u", "Smart Undo"), _row("p", "Plan Command (AI)"))
212
+ menu.add_row(_row("s", "Repo Stats"), _row("w", "Switch Repo"))
213
+ menu.add_row(_row("q", "Quit"), Text(""))
212
214
 
213
215
  console.print(
214
216
  Panel(menu, title="[bold white]Actions[/bold white]",
@@ -30,9 +30,65 @@ def print_warning(message: str) -> None:
30
30
  """Print a warning message."""
31
31
  console.print(f" [warning]{_SYM_WARNING}[/warning] [warning]{message}[/warning]")
32
32
 
33
+ def clean_error_message(message: str) -> str:
34
+ """Clean up common LLM API response dumps and traceback details into human-readable text."""
35
+ from rich.markup import escape
36
+
37
+ # Normalize/clean raw message string
38
+ msg_str = str(message).strip()
39
+
40
+ # Check for NVIDIA/OpenAI 504 gateway timeout
41
+ if "504" in msg_str or "Gateway Timeout" in msg_str:
42
+ return (
43
+ "API Server Error: [bold #FF1744]Gateway Timeout (504)[/bold #FF1744]\n\n"
44
+ "The cloud AI provider servers took too long to respond. This is a temporary server "
45
+ "overload. Please try again in a few moments, or run [bold]ace setup[/bold] to switch "
46
+ "to a local model (Ollama) or another provider."
47
+ )
48
+
49
+ # Check for authentication errors
50
+ if "AuthenticationError" in msg_str or "invalid api key" in msg_str.lower() or "401" in msg_str:
51
+ return (
52
+ "API Authentication Error: [bold #FF1744]Invalid API Key[/bold #FF1744]\n\n"
53
+ "Please check that your API key is correct and active. Run [bold]ace setup[/bold] to reconfigure."
54
+ )
55
+
56
+ # Check for rate limits
57
+ if "RateLimitError" in msg_str or "429" in msg_str or "rate limit exceeded" in msg_str.lower():
58
+ return (
59
+ "API Rate Limit Error: [bold #FF1744]Too Many Requests[/bold #FF1744]\n\n"
60
+ "You have hit the rate limit for this API provider. Please wait a moment before trying again."
61
+ )
62
+
63
+ # General Connection errors
64
+ if "ConnectionError" in msg_str or "Failed to establish a new connection" in msg_str or "timeout" in msg_str.lower():
65
+ return (
66
+ "API Connection Error: [bold #FF1744]Network Timeout[/bold #FF1744]\n\n"
67
+ "Could not connect to the AI API endpoint. Please check your internet connection and try again."
68
+ )
69
+
70
+ # If it's a raw dictionary dump of a response (like in NVIDIA / OpenAI SDK exception strings)
71
+ if "{'_content':" in msg_str or "'status_code':" in msg_str or "'_content_consumed':" in msg_str:
72
+ lines = msg_str.splitlines()
73
+ first_line = lines[0] if lines else ""
74
+ if "APIStatusError" in first_line:
75
+ first_line = first_line.split("APIStatusError:")[-1].strip()
76
+
77
+ import re
78
+ status_match = re.search(r"'status_code':\s*(\d+)", msg_str)
79
+ reason_match = re.search(r"'reason':\s*'([^']+)'", msg_str)
80
+ if status_match and reason_match:
81
+ return f"API Error: [bold #FF1744]{escape(reason_match.group(1))} ({status_match.group(1)})[/bold #FF1744]"
82
+ elif status_match:
83
+ return f"API Error: [bold #FF1744]Status Code {status_match.group(1)}[/bold #FF1744]"
84
+ return escape(first_line) if first_line else "An unexpected API error occurred."
85
+
86
+ # If it's a generic exception or string, escape it to prevent Rich markup parsing errors
87
+ return escape(msg_str)
88
+
33
89
  def print_error(message: str) -> None:
34
90
  """Print an error message."""
35
- err_console.print(f" [error]{_SYM_ERROR}[/error] [error]{message}[/error]")
91
+ err_console.print(f" [error]{_SYM_ERROR}[/error] [error]{clean_error_message(message)}[/error]")
36
92
 
37
93
 
38
94
  # ─── Panels ──────────────────────────────────────────────────────────────────
@@ -57,8 +113,11 @@ def show_error_panel(message: str, title: str = "Error") -> None:
57
113
  """Show a styled red error panel."""
58
114
  from rich.panel import Panel
59
115
  from rich import box
116
+
117
+ cleaned_message = clean_error_message(message)
118
+
60
119
  panel = Panel(
61
- Text.from_markup(message),
120
+ Text.from_markup(cleaned_message),
62
121
  title=f"[bold #FF1744] {_SYM_ERROR} {title}[/bold #FF1744]",
63
122
  border_style="#FF1744",
64
123
  box=box.ROUNDED,
@@ -13,7 +13,7 @@ def confirm(question: str, default: bool = True) -> bool:
13
13
  hint = "[Y/n]" if default else "[y/N]"
14
14
  console.print(
15
15
  Text.assemble(
16
- (" ", "bold #00D5FF"),
16
+ (" >> ", "bold #00D5FF"),
17
17
  (question + " ", "bold white"),
18
18
  (hint, "bold #666666"),
19
19
  (" ", ""),
@@ -30,7 +30,8 @@ def confirm(question: str, default: bool = True) -> bool:
30
30
  return False
31
31
  if val in ("\r", "\n", ""):
32
32
  return default
33
- return default
33
+ # Default to False for safety on unrecognized inputs
34
+ return False
34
35
 
35
36
 
36
37
  def prompt_action(options: Dict[str, Tuple[str, str]], default_key: str = "\r") -> str:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ace-git-copilot"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "AI-powered Git copilot — talk to Git in plain English"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -13,8 +13,19 @@ class MockLLMHandler(BaseHTTPRequestHandler):
13
13
  # Suppress request logging to keep output clean
14
14
  pass
15
15
 
16
+ def do_GET(self):
17
+ if self.path == "/api/tags":
18
+ self.send_response(200)
19
+ self.send_header("Content-Type", "application/json")
20
+ self.end_headers()
21
+ self.wfile.write(json.dumps({"models": [{"name": "qwen2.5-coder:7b"}]}).encode('utf-8'))
22
+ else:
23
+ self.send_response(404)
24
+ self.end_headers()
25
+
16
26
  def do_POST(self):
17
- if self.path == "/v1/chat/completions":
27
+ is_ollama = (self.path == "/api/chat")
28
+ if self.path == "/v1/chat/completions" or is_ollama:
18
29
  content_length = int(self.headers['Content-Length'])
19
30
  post_data = self.rfile.read(content_length)
20
31
  payload = json.loads(post_data.decode('utf-8'))
@@ -69,7 +80,14 @@ class MockLLMHandler(BaseHTTPRequestHandler):
69
80
  "risk_level": "destructive",
70
81
  "alternatives": "git stash"
71
82
  })
72
- elif "invalid" in query_val or "unrelated" in query_val:
83
+ elif "config" in query_val:
84
+ response_content = json.dumps({
85
+ "commands": ["ace config"],
86
+ "explanation": "Show active configuration.",
87
+ "risk_level": "safe",
88
+ "alternatives": None
89
+ })
90
+ elif "invalid" in query_val or "unrelated" in query_val or "coffee" in query_val or "make me" in query_val:
73
91
  response_content = json.dumps({
74
92
  "commands": [],
75
93
  "explanation": "I cannot parse this command.",
@@ -109,6 +127,10 @@ class MockLLMHandler(BaseHTTPRequestHandler):
109
127
  "alternatives": None
110
128
  })
111
129
 
130
+ # 3. Changelog Generator
131
+ elif "release coordinator and technical writer" in full_text or "Markdown changelog from the provided Git commit log" in full_text or "changelog" in full_text.lower():
132
+ response_content = "# Changelog\n\n## [1.0.0]\n\n### ✨ Features\n- Add mock feature\n\n### 🐛 Bug Fixes\n- Fix mock bug"
133
+
112
134
  # 2. Commit Message Generator
113
135
  elif "Conventional Commits" in full_text or "commit message" in full_text or "Staged Diff" in full_text:
114
136
  if "one-line commit message" in full_text or "SIMPLE_COMMIT_SYSTEM_PROMPT" in full_text:
@@ -118,10 +140,6 @@ class MockLLMHandler(BaseHTTPRequestHandler):
118
140
  else:
119
141
  response_content = "feat(mock): add mock feature\n\n- Implement mock feature details\n- Add mock feature tests"
120
142
 
121
- # 3. Changelog Generator
122
- elif "release coordinator and technical writer" in full_text or "Markdown changelog from the provided Git commit log" in full_text:
123
- response_content = "# Changelog\n\n## [1.0.0]\n\n### ✨ Features\n- Add mock feature\n\n### 🐛 Bug Fixes\n- Fix mock bug"
124
-
125
143
  # 4. PR Drafter
126
144
  elif "PR_SYSTEM_PROMPT" in full_text or "Pull Request (PR) description" in full_text:
127
145
  response_content = json.dumps({
@@ -134,54 +152,64 @@ class MockLLMHandler(BaseHTTPRequestHandler):
134
152
  response_content = "🩺 **Diagnostics Assessment**\n\nFound some issues.\n\n📋 **Recovery Plan**\n\n- Run git clean\n- Run git restore\n\n💡 **Prevention Tip**\n\nCommit more often."
135
153
 
136
154
  # 6. Smart Undo
137
- elif "UNDO_SYSTEM_PROMPT" in full_text or "reflog_entries" in full_text:
138
- if "test_staged.txt" in full_text or "staged_files" in full_text and "None" not in full_text:
139
- response_content = json.dumps({
140
- "commands": ["git restore --staged ."],
141
- "explanation": "Unstage changes.",
142
- "risk_level": "moderate",
143
- "alternatives": None
144
- })
145
- elif "destructive" in full_text or "hard" in full_text:
146
- response_content = json.dumps({
147
- "commands": ["git reset --hard ORIG_HEAD"],
148
- "explanation": "Destructively undo merge.",
149
- "risk_level": "destructive",
150
- "alternatives": "git stash"
151
- })
152
- elif "nothing" in full_text or "clean" in full_text:
155
+ elif "Active Operations:" in full_text or "recent Git reflog" in full_text:
156
+ if "No reflog available" in full_text or ("Staged Changes:\nNone" in full_text and "Unstaged Changes:\nNone" in full_text):
153
157
  response_content = json.dumps({
154
158
  "commands": [],
155
159
  "explanation": "Nothing to undo.",
156
160
  "risk_level": "safe",
157
161
  "alternatives": None
158
162
  })
159
- else:
163
+ elif "test_staged.txt" in full_text or "test.txt" in full_text:
164
+ response_content = json.dumps({
165
+ "commands": ["git restore --staged ."],
166
+ "explanation": "Unstage changes.",
167
+ "risk_level": "moderate",
168
+ "alternatives": None
169
+ })
170
+ elif "update commit" in full_text:
160
171
  response_content = json.dumps({
161
172
  "commands": ["git reset --soft HEAD~1"],
162
173
  "explanation": "Undo last commit.",
163
174
  "risk_level": "moderate",
164
175
  "alternatives": None
165
176
  })
177
+ else:
178
+ response_content = json.dumps({
179
+ "commands": ["git reset --hard ORIG_HEAD"],
180
+ "explanation": "Destructively undo merge.",
181
+ "risk_level": "destructive",
182
+ "alternatives": "git stash"
183
+ })
166
184
 
167
185
  self.send_response(200)
168
186
  self.send_header("Content-Type", "application/json")
169
187
  self.end_headers()
170
188
 
171
- response_payload = {
172
- "id": "chatcmpl-mock",
173
- "object": "chat.completion",
174
- "created": 1677652288,
175
- "model": "mock-model",
176
- "choices": [{
177
- "index": 0,
189
+ if is_ollama:
190
+ response_payload = {
191
+ "model": "qwen2.5-coder:7b",
178
192
  "message": {
179
193
  "role": "assistant",
180
194
  "content": response_content
181
195
  },
182
- "finish_reason": "stop"
183
- }]
184
- }
196
+ "done": True
197
+ }
198
+ else:
199
+ response_payload = {
200
+ "id": "chatcmpl-mock",
201
+ "object": "chat.completion",
202
+ "created": 1677652288,
203
+ "model": "mock-model",
204
+ "choices": [{
205
+ "index": 0,
206
+ "message": {
207
+ "role": "assistant",
208
+ "content": response_content
209
+ },
210
+ "finish_reason": "stop"
211
+ }]
212
+ }
185
213
  self.wfile.write(json.dumps(response_payload).encode('utf-8'))
186
214
  else:
187
215
  self.send_response(404)
@@ -232,13 +260,14 @@ def git_workspace(tmp_path, mock_llm_port):
232
260
  cmd = [
233
261
  sys.executable,
234
262
  "-c",
235
- "import sys, click; click.getchar = lambda: sys.stdin.read(1); from ace.cli import app; app()",
263
+ "import sys, click, getpass; click.getchar = lambda: sys.stdin.read(1); getpass.getpass = lambda prompt='', stream=None: sys.stdin.readline().rstrip('\\r\\n'); from ace.cli import app; app()",
236
264
  ] + args
237
265
  env = os.environ.copy()
238
266
  env["ACE_PROVIDER"] = "custom"
239
267
  env["CUSTOM_API_BASE"] = f"http://127.0.0.1:{self.port}/v1"
240
268
  env["CUSTOM_API_KEY"] = "mock-key"
241
269
  env["CUSTOM_MODEL"] = "mock-model"
270
+ env["OLLAMA_URL"] = f"http://127.0.0.1:{self.port}"
242
271
  env["HOME"] = str(self.home)
243
272
  env["USERPROFILE"] = str(self.home)
244
273
 
@@ -165,8 +165,8 @@ def test_changelog_display(git_workspace):
165
165
 
166
166
  res = git_workspace.run(["changelog"])
167
167
  assert res.returncode == 0
168
- assert "## [1.0.0]" in res.stdout
169
- assert "### ✨ Features" in res.stdout
168
+ assert "[1.0.0]" in res.stdout
169
+ assert "Features" in res.stdout
170
170
 
171
171
  def test_changelog_output_file(git_workspace):
172
172
  test_file = git_workspace.workspace / "test.txt"
@@ -178,9 +178,15 @@ def test_changelog_output_file(git_workspace):
178
178
  res = git_workspace.run(["changelog", "-o", str(out_file)])
179
179
  assert res.returncode == 0
180
180
  assert out_file.exists()
181
- assert "## [1.0.0]" in out_file.read_text()
181
+ assert "## [1.0.0]" in out_file.read_text(encoding="utf-8")
182
182
 
183
183
  def test_changelog_range(git_workspace):
184
+ # Create initial commit so HEAD~1 exists
185
+ dummy_file = git_workspace.workspace / "dummy.txt"
186
+ dummy_file.write_text("dummy")
187
+ git_workspace.repo.index.add([str(dummy_file)])
188
+ git_workspace.repo.index.commit("initial")
189
+
184
190
  test_file = git_workspace.workspace / "test.txt"
185
191
  test_file.write_text("changelog content 3")
186
192
  git_workspace.repo.index.add([str(test_file)])
@@ -188,7 +194,7 @@ def test_changelog_range(git_workspace):
188
194
 
189
195
  res = git_workspace.run(["changelog", "--from", "HEAD~1", "--to", "HEAD"])
190
196
  assert res.returncode == 0
191
- assert "## [1.0.0]" in res.stdout
197
+ assert "[1.0.0]" in res.stdout
192
198
 
193
199
  def test_changelog_offline(git_workspace):
194
200
  test_file = git_workspace.workspace / "test.txt"
@@ -198,7 +204,7 @@ def test_changelog_offline(git_workspace):
198
204
 
199
205
  res = git_workspace.run(["changelog", "--offline"])
200
206
  assert res.returncode == 0
201
- assert "## [1.0.0]" in res.stdout
207
+ assert "[1.0.0]" in res.stdout
202
208
 
203
209
  def test_changelog_empty(git_workspace):
204
210
  # Fresh repository with no commits
@@ -340,9 +346,15 @@ def test_undo_commit(git_workspace):
340
346
 
341
347
  # Commit should be undone, leaving 1 commit and changes staged
342
348
  assert len(list(git_workspace.repo.iter_commits())) == 1
343
- assert "test.txt" in git_workspace.repo.index.diff("HEAD")
349
+ assert "test.txt" in [diff.a_path for diff in git_workspace.repo.index.diff("HEAD")]
344
350
 
345
351
  def test_undo_staged(git_workspace):
352
+ # Create initial commit so HEAD exists
353
+ dummy_file = git_workspace.workspace / "dummy.txt"
354
+ dummy_file.write_text("dummy")
355
+ git_workspace.repo.index.add([str(dummy_file)])
356
+ git_workspace.repo.index.commit("initial")
357
+
346
358
  test_file = git_workspace.workspace / "test_staged.txt"
347
359
  test_file.write_text("staged content")
348
360
  git_workspace.repo.index.add([str(test_file)])