ace-git-copilot 0.4.4__tar.gz → 0.4.6__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 (139) hide show
  1. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/PKG-INFO +1 -1
  2. ace_git_copilot-0.4.6/ace/__init__.py +1 -0
  3. ace_git_copilot-0.4.6/ace/ai/push_resolver.py +172 -0
  4. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/cli.py +19 -8
  5. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/pyproject.toml +1 -1
  6. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_interceptor.py +20 -0
  7. ace_git_copilot-0.4.6/tests/test_push_resolver.py +32 -0
  8. ace_git_copilot-0.4.4/ace/__init__.py +0 -1
  9. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/AGENTS.md +0 -0
  10. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/BRIEFING.md +0 -0
  11. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/ORIGINAL_REQUEST.md +0 -0
  12. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/e2e_testing_track/BRIEFING.md +0 -0
  13. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/e2e_testing_track/ORIGINAL_REQUEST.md +0 -0
  14. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/e2e_testing_track/SCOPE.md +0 -0
  15. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/e2e_testing_track/progress.md +0 -0
  16. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/BRIEFING.md +0 -0
  17. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/ORIGINAL_REQUEST.md +0 -0
  18. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/emojis_list.txt +0 -0
  19. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/find_unused_modules.py +0 -0
  20. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/handoff.md +0 -0
  21. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/measure_lazy_startup.py +0 -0
  22. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/measure_startup.py +0 -0
  23. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/profile_imports.py +0 -0
  24. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/progress.md +0 -0
  25. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/run_importtime.py +0 -0
  26. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/search_banner.py +0 -0
  27. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/search_emojis.py +0 -0
  28. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/search_git_usages.py +0 -0
  29. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/search_usages.py +0 -0
  30. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/test_import_profiler.py +0 -0
  31. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/explorer_init/test_mocked_sys.py +0 -0
  32. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/handoff.md +0 -0
  33. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/implementation_track/BRIEFING.md +0 -0
  34. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/implementation_track/ORIGINAL_REQUEST.md +0 -0
  35. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/implementation_track/explorer_initial_report.md +0 -0
  36. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/implementation_track/progress.md +0 -0
  37. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/orchestrator/.gitkeep +0 -0
  38. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/orchestrator/BRIEFING.md +0 -0
  39. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/orchestrator/ORIGINAL_REQUEST.md +0 -0
  40. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/orchestrator/PROJECT.md +0 -0
  41. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/orchestrator/progress.md +0 -0
  42. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/teamwork_preview_explorer_e2e_explore/BRIEFING.md +0 -0
  43. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/teamwork_preview_explorer_e2e_explore/ORIGINAL_REQUEST.md +0 -0
  44. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/teamwork_preview_explorer_e2e_explore/handoff.md +0 -0
  45. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/teamwork_preview_explorer_e2e_explore/progress.md +0 -0
  46. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/worker_e2e_testing/BRIEFING.md +0 -0
  47. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/worker_e2e_testing/ORIGINAL_REQUEST.md +0 -0
  48. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/worker_e2e_testing/progress.md +0 -0
  49. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/worker_m1_startup/BRIEFING.md +0 -0
  50. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/worker_m1_startup/ORIGINAL_REQUEST.md +0 -0
  51. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.agents/worker_m1_startup/progress.md +0 -0
  52. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.env.example +0 -0
  53. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.github/workflows/tests.yml +0 -0
  54. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/.gitignore +0 -0
  55. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/CODE_OF_CONDUCT.md +0 -0
  56. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/CONTRIBUTING.md +0 -0
  57. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/LICENSE +0 -0
  58. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/PROJECT.md +0 -0
  59. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/README.md +0 -0
  60. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/SECURITY.md +0 -0
  61. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/SUPPORT.md +0 -0
  62. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/TEST_INFRA.md +0 -0
  63. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/TEST_READY.md +0 -0
  64. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/__main__.py +0 -0
  65. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/changelog_generator.py +0 -0
  66. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/code_reviewer.py +0 -0
  67. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/commit_generator.py +0 -0
  68. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/conflict_resolver.py +0 -0
  69. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/gitignore_generator.py +0 -0
  70. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/history_analyzer.py +0 -0
  71. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/intent_parser.py +0 -0
  72. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/llm_factory.py +0 -0
  73. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/pr_drafter.py +0 -0
  74. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/blame.py +0 -0
  75. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/changelog.py +0 -0
  76. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/commit.py +0 -0
  77. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/conflict.py +0 -0
  78. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/doctor.py +0 -0
  79. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/explain.py +0 -0
  80. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/ignore.py +0 -0
  81. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/intent.py +0 -0
  82. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/pr.py +0 -0
  83. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/rebase.py +0 -0
  84. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/review.py +0 -0
  85. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/search.py +0 -0
  86. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/standup.py +0 -0
  87. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/prompts/undo.py +0 -0
  88. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ai/rebase_helper.py +0 -0
  89. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/core/config.py +0 -0
  90. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/core/context.py +0 -0
  91. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/core/diagnostics.py +0 -0
  92. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/core/git_ops.py +0 -0
  93. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/core/hooks.py +0 -0
  94. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/core/safety.py +0 -0
  95. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ui/banner.py +0 -0
  96. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ui/dashboard.py +0 -0
  97. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ui/display.py +0 -0
  98. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ui/prompts.py +0 -0
  99. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/ui/themes.py +0 -0
  100. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/utils/conflict_parser.py +0 -0
  101. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/utils/diff_parser.py +0 -0
  102. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/utils/json_utils.py +0 -0
  103. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/ace/utils/string_utils.py +0 -0
  104. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/media/cli_in_action.svg +0 -0
  105. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/media/dashboard.svg +0 -0
  106. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/media/stats.svg +0 -0
  107. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/conftest.py +0 -0
  108. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/e2e/conftest.py +0 -0
  109. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/e2e/test_tier1_features.py +0 -0
  110. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/e2e/test_tier2_boundaries.py +0 -0
  111. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/e2e/test_tier3_combinations.py +0 -0
  112. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/e2e/test_tier4_workloads.py +0 -0
  113. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_blame.py +0 -0
  114. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_changelog_generator.py +0 -0
  115. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_code_reviewer.py +0 -0
  116. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_config_atomic_write.py +0 -0
  117. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_conflict_resolver.py +0 -0
  118. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_conflict_resolver_backup.py +0 -0
  119. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_context_gitdir_detection.py +0 -0
  120. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_diagnostics.py +0 -0
  121. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_diff_trimmer.py +0 -0
  122. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_git_ops.py +0 -0
  123. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_git_ops_execute.py +0 -0
  124. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_help.py +0 -0
  125. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_history_analyzer.py +0 -0
  126. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_hooks.py +0 -0
  127. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_ignore.py +0 -0
  128. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_intent_parser.py +0 -0
  129. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_llm_factory.py +0 -0
  130. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_llm_factory_ollama.py +0 -0
  131. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_merge_state.py +0 -0
  132. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_pr_drafter.py +0 -0
  133. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_rebase_helper.py +0 -0
  134. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_safety.py +0 -0
  135. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_search.py +0 -0
  136. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_standup.py +0 -0
  137. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_string_utils.py +0 -0
  138. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_undo.py +0 -0
  139. {ace_git_copilot-0.4.4 → ace_git_copilot-0.4.6}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ace-git-copilot
3
- Version: 0.4.4
3
+ Version: 0.4.6
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
@@ -0,0 +1 @@
1
+ __version__ = "0.4.6"
@@ -0,0 +1,172 @@
1
+ from typing import Dict, Any, List, Optional
2
+ from rich.panel import Panel
3
+ from rich.text import Text
4
+ from rich import box
5
+ import typer
6
+
7
+ from ace.core.git_ops import GitOps
8
+ from ace.ui.display import console, spinner, print_success, print_warning, show_error_panel
9
+
10
+ def categorize_push_error(error_msg: str) -> Dict[str, Any]:
11
+ """Categorize git push error into actionable problem types and remedies."""
12
+ err_lower = error_msg.lower()
13
+
14
+ if any(pattern in err_lower for pattern in ["fetch first", "non-fast-forward", "behind", "rejected", "updates were rejected because the remote contains work"]):
15
+ return {
16
+ "type": "non_fast_forward",
17
+ "title": "Remote Branch Has New Commits (Non-Fast-Forward)",
18
+ "description": "The remote branch contains commits that you do not have locally. You must pull remote changes before pushing.",
19
+ "actions": [
20
+ {"key": "1", "label": "Pull & Rebase remote changes (git pull --rebase)", "cmd": "pull_rebase"},
21
+ {"key": "2", "label": "Pull & Merge remote changes (git pull)", "cmd": "pull_merge"},
22
+ {"key": "3", "label": "Force push safely with lease (git push --force-with-lease)", "cmd": "force_lease"},
23
+ ]
24
+ }
25
+ elif any(pattern in err_lower for pattern in ["no upstream", "has no upstream", "set-upstream"]):
26
+ return {
27
+ "type": "no_upstream",
28
+ "title": "Missing Upstream Tracking Branch",
29
+ "description": "Your local branch does not have an upstream tracking branch on the remote.",
30
+ "actions": [
31
+ {"key": "1", "label": "Set upstream and push (git push -u)", "cmd": "set_upstream"},
32
+ ]
33
+ }
34
+ elif any(pattern in err_lower for pattern in ["permission denied", "authentication failed", "could not read from remote", "publickey"]):
35
+ return {
36
+ "type": "permission_denied",
37
+ "title": "Authentication / Access Permission Denied",
38
+ "description": "Git could not authenticate with the remote repository. Check your SSH keys or HTTPS access token.",
39
+ "actions": [
40
+ {"key": "1", "label": "Show SSH / Token troubleshooting guide", "cmd": "help_auth"},
41
+ ]
42
+ }
43
+ else:
44
+ return {
45
+ "type": "unknown",
46
+ "title": "Git Push Error",
47
+ "description": error_msg.strip()[:200],
48
+ "actions": [
49
+ {"key": "1", "label": "Pull & Rebase remote changes (git pull --rebase)", "cmd": "pull_rebase"},
50
+ {"key": "2", "label": "Force push safely with lease (git push --force-with-lease)", "cmd": "force_lease"},
51
+ ]
52
+ }
53
+
54
+ def handle_push_failure(git_ops: GitOps, error_msg: str, remote: str = "origin", branch: Optional[str] = None, offline: bool = False) -> bool:
55
+ """
56
+ Interactively diagnose a git push failure and prompt the user with recovery options.
57
+ Returns True if the push was resolved successfully, False otherwise.
58
+ """
59
+ curr_branch = branch or git_ops.get_current_branch() or "master"
60
+ category = categorize_push_error(error_msg)
61
+
62
+ console.print()
63
+ diag_text = Text()
64
+ diag_text.append(f"Problem: {category['description']}\n", style="white")
65
+ diag_text.append(f"Target Branch: {remote}/{curr_branch}\n", style="dim #9E9E9E")
66
+
67
+ console.print(Panel(
68
+ diag_text,
69
+ title=f"[bold #FF1744]✘ {category['title']}[/bold #FF1744]",
70
+ border_style="#FF1744",
71
+ padding=(1, 2)
72
+ ))
73
+
74
+ console.print("[bold cyan]Choose resolution action:[/bold cyan]")
75
+ for act in category["actions"]:
76
+ console.print(f" [{act['key']}] {act['label']}")
77
+ console.print(" [s] Skip / Cancel push")
78
+
79
+ choice = typer.prompt("Select option", default="1").lower().strip()
80
+
81
+ if choice == "s":
82
+ print_warning("Push resolution cancelled.")
83
+ return False
84
+
85
+ selected_act = None
86
+ for act in category["actions"]:
87
+ if act["key"] == choice:
88
+ selected_act = act
89
+ break
90
+
91
+ if not selected_act and category["actions"]:
92
+ selected_act = category["actions"][0]
93
+
94
+ if not selected_act:
95
+ return False
96
+
97
+ cmd_type = selected_act["cmd"]
98
+
99
+ if cmd_type == "pull_rebase":
100
+ try:
101
+ with spinner(f"Pulling and rebasing from {remote}/{curr_branch}..."):
102
+ git_ops.execute(f"pull --rebase {remote} {curr_branch}")
103
+ print_success("Successfully pulled and rebased remote changes!")
104
+
105
+ # Retry push
106
+ with spinner(f"Retrying push to {remote}/{curr_branch}..."):
107
+ push_res = git_ops.push(remote=remote, branch=curr_branch)
108
+ print_success("Pushed to remote successfully!")
109
+ if push_res.strip():
110
+ console.print(f"[dim]{push_res}[/dim]")
111
+ return True
112
+ except Exception as e:
113
+ show_error_panel(f"Failed during pull & rebase or retry push: {e}", "Resolution Error")
114
+ return False
115
+
116
+ elif cmd_type == "pull_merge":
117
+ try:
118
+ with spinner(f"Pulling and merging from {remote}/{curr_branch}..."):
119
+ git_ops.execute(f"pull {remote} {curr_branch}")
120
+ print_success("Successfully merged remote changes!")
121
+
122
+ # Retry push
123
+ with spinner(f"Retrying push to {remote}/{curr_branch}..."):
124
+ push_res = git_ops.push(remote=remote, branch=curr_branch)
125
+ print_success("Pushed to remote successfully!")
126
+ if push_res.strip():
127
+ console.print(f"[dim]{push_res}[/dim]")
128
+ return True
129
+ except Exception as e:
130
+ show_error_panel(f"Failed during pull or retry push: {e}", "Resolution Error")
131
+ return False
132
+
133
+ elif cmd_type == "force_lease":
134
+ from ace.ui.prompts import confirm
135
+ if confirm("Force push with lease will overwrite remote commits if your local branch is out of sync. Continue?", default=False):
136
+ try:
137
+ with spinner(f"Force pushing (--force-with-lease) to {remote}/{curr_branch}..."):
138
+ push_res = git_ops.execute(f"push --force-with-lease {remote} {curr_branch}")
139
+ print_success("Force pushed with lease successfully!")
140
+ if push_res.strip():
141
+ console.print(f"[dim]{push_res}[/dim]")
142
+ return True
143
+ except Exception as e:
144
+ show_error_panel(f"Force push failed: {e}", "Resolution Error")
145
+ return False
146
+ else:
147
+ print_warning("Force push cancelled.")
148
+ return False
149
+
150
+ elif cmd_type == "set_upstream":
151
+ try:
152
+ with spinner(f"Setting upstream and pushing to {remote}/{curr_branch}..."):
153
+ push_res = git_ops.push(remote=remote, branch=curr_branch, set_upstream=True)
154
+ print_success("Set upstream and pushed successfully!")
155
+ if push_res.strip():
156
+ console.print(f"[dim]{push_res}[/dim]")
157
+ return True
158
+ except Exception as e:
159
+ show_error_panel(f"Push with set-upstream failed: {e}", "Resolution Error")
160
+ return False
161
+
162
+ elif cmd_type == "help_auth":
163
+ auth_guide = (
164
+ "Authentication Troubleshooting Guide:\n"
165
+ "1. SSH Key setup: Run 'ssh -T git@github.com' to verify key access.\n"
166
+ "2. HTTPS Token: Run 'git config --global credential.helper manager' to update saved passwords/tokens.\n"
167
+ "3. Remote URL: Verify remote URL with 'git remote -v'."
168
+ )
169
+ console.print(Panel(auth_guide, title="[bold cyan]Authentication Help[/bold cyan]", border_style="#00D5FF"))
170
+ return False
171
+
172
+ return False
@@ -86,17 +86,25 @@ def main(
86
86
 
87
87
  # Intercept subcommand if the user typed an English sentence (e.g. 'ace add and commit')
88
88
  intercepted_query = None
89
- if ctx.invoked_subcommand in ("add", "stage", "commit", "review", "explain", "resolve", "search"):
89
+ ALL_INTERCEPT_SUBCOMMANDS = (
90
+ "add", "stage", "commit", "review", "explain", "resolve", "search",
91
+ "undo", "workspace", "ws", "standup", "blame", "pr", "squash", "ignore", "doctor"
92
+ )
93
+ if ctx.invoked_subcommand in ALL_INTERCEPT_SUBCOMMANDS:
90
94
  import sys
91
95
  sub_cmd = ctx.invoked_subcommand
92
96
  sub_idx = -1
93
- for idx, arg in enumerate(sys.argv):
94
- if arg == sub_cmd and idx > 0 and not sys.argv[idx - 1].startswith("-"):
97
+ for idx, arg in enumerate(sys.argv[1:], start=1):
98
+ if arg == sub_cmd:
95
99
  sub_idx = idx
96
100
  break
97
101
  if sub_idx != -1:
98
102
  sub_args = sys.argv[sub_idx + 1:]
99
- nl_indicators = {"and", "with", "to", "the", "a", "new", "my", "all", "but", "yesterday", "last", "from", "for", "changed", "everything"}
103
+ nl_indicators = {
104
+ "and", "with", "to", "the", "a", "an", "in", "on", "new", "my", "all",
105
+ "but", "yesterday", "last", "from", "for", "changed", "everything",
106
+ "changes", "repo", "repos", "commits", "files"
107
+ }
100
108
  if any(arg.lower() in nl_indicators for arg in sub_args):
101
109
  query_parts = []
102
110
  for arg in sys.argv[1:]:
@@ -105,6 +113,7 @@ def main(
105
113
  query_parts.append(arg)
106
114
  intercepted_query = " ".join(query_parts)
107
115
 
116
+
108
117
  if intercepted_query:
109
118
  query = intercepted_query
110
119
  elif ctx.invoked_subcommand is not None:
@@ -447,20 +456,22 @@ def commit_cmd(
447
456
  print_success("Pushed and set upstream branch successfully!")
448
457
  console.print(f"[dim]{push_res}[/dim]")
449
458
  except Exception as e:
450
- show_error_panel(f"Push failed: {e}", "Git Push Error")
459
+ from ace.ai.push_resolver import handle_push_failure
460
+ handle_push_failure(git_ops, str(e), remote=selected_remote, branch=current_branch, offline=offline)
451
461
  else:
452
462
  # Upstream exists
453
463
  msg_push = f"Push to upstream branch '{upstream}'? (Your branch is {ahead} commit(s) ahead of remote)"
454
464
  if confirm(msg_push, default=True):
465
+ remote_name = upstream.split("/")[0] if "/" in upstream else "origin"
455
466
  try:
456
- # Find the remote name from upstream (e.g. 'origin/main' -> 'origin')
457
- remote_name = upstream.split("/")[0] if "/" in upstream else "origin"
458
467
  with spinner(f"Pushing to {upstream}..."):
459
468
  push_res = git_ops.push(remote=remote_name)
460
469
  print_success("Pushed to remote successfully!")
461
470
  console.print(f"[dim]{push_res}[/dim]")
462
471
  except Exception as e:
463
- show_error_panel(f"Push failed: {e}", "Git Push Error")
472
+ from ace.ai.push_resolver import handle_push_failure
473
+ handle_push_failure(git_ops, str(e), remote=remote_name, branch=current_branch, offline=offline)
474
+
464
475
 
465
476
  @app.command(name="setup", help="Initial configuration wizard for Ace.")
466
477
  def setup_cmd():
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ace-git-copilot"
3
- version = "0.4.4"
3
+ version = "0.4.6"
4
4
  description = "AI-powered Git copilot — talk to Git in plain English"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -40,3 +40,23 @@ def test_subcommand_interceptor_normal_subcmd(mock_git_ops_class):
40
40
  # Normal execution path: it routes to the add subcommand, tries to run git add, and fails
41
41
  assert result.exit_code == 1
42
42
  mock_git_ops.execute.assert_called_once_with("add file1.txt")
43
+
44
+
45
+ @patch("ace.core.git_ops.GitOps")
46
+ @patch("ace.ai.llm_factory.get_llm")
47
+ @patch("ace.ai.intent_parser.IntentParser")
48
+ def test_subcommand_interceptor_with_flags(mock_parser_class, mock_get_llm, mock_git_ops_class):
49
+ mock_git_ops = MagicMock()
50
+ mock_git_ops_class.return_value = mock_git_ops
51
+
52
+ mock_parser = MagicMock()
53
+ mock_parser.parse_intent.return_value = {"commands": [], "explanation": "NL query with flag matched"}
54
+ mock_parser_class.return_value = mock_parser
55
+
56
+ # Simulate: ace -y add and commit
57
+ with patch.object(sys, "argv", ["ace", "-y", "add", "and", "commit"]):
58
+ result = runner.invoke(app, ["-y", "add", "and", "commit"])
59
+
60
+ assert result.exit_code == 0
61
+ mock_parser.parse_intent.assert_called_once_with("add and commit", offline=False)
62
+
@@ -0,0 +1,32 @@
1
+ from unittest.mock import MagicMock, patch
2
+ from ace.ai.push_resolver import categorize_push_error, handle_push_failure
3
+
4
+ def test_categorize_push_error_non_fast_forward():
5
+ err = "error: failed to push some refs to 'github.com:repo/app.git'\nhint: Updates were rejected because the remote contains work that you do not have locally."
6
+ cat = categorize_push_error(err)
7
+ assert cat["type"] == "non_fast_forward"
8
+ assert len(cat["actions"]) == 3
9
+
10
+ def test_categorize_push_error_no_upstream():
11
+ err = "fatal: The current branch master has no upstream branch."
12
+ cat = categorize_push_error(err)
13
+ assert cat["type"] == "no_upstream"
14
+ assert cat["actions"][0]["cmd"] == "set_upstream"
15
+
16
+ def test_categorize_push_error_permission_denied():
17
+ err = "Permission denied (publickey). fatal: Could not read from remote repository."
18
+ cat = categorize_push_error(err)
19
+ assert cat["type"] == "permission_denied"
20
+
21
+ @patch("ace.ai.push_resolver.typer.prompt", return_value="1")
22
+ def test_handle_push_failure_pull_rebase(mock_prompt):
23
+ mock_git_ops = MagicMock()
24
+ mock_git_ops.get_current_branch.return_value = "master"
25
+ mock_git_ops.push.return_value = "Everything up-to-date"
26
+
27
+ err = "Updates were rejected because the remote contains work"
28
+ res = handle_push_failure(mock_git_ops, err, remote="origin", branch="master")
29
+
30
+ assert res is True
31
+ mock_git_ops.execute.assert_called_once_with("pull --rebase origin master")
32
+ mock_git_ops.push.assert_called_once_with(remote="origin", branch="master")
@@ -1 +0,0 @@
1
- __version__ = "0.4.4"
File without changes