cecli-dev 0.95.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. cecli/__init__.py +20 -0
  2. cecli/__main__.py +4 -0
  3. cecli/_version.py +34 -0
  4. cecli/args.py +1092 -0
  5. cecli/args_formatter.py +228 -0
  6. cecli/change_tracker.py +133 -0
  7. cecli/coders/__init__.py +38 -0
  8. cecli/coders/agent_coder.py +1872 -0
  9. cecli/coders/architect_coder.py +63 -0
  10. cecli/coders/ask_coder.py +8 -0
  11. cecli/coders/base_coder.py +3993 -0
  12. cecli/coders/chat_chunks.py +116 -0
  13. cecli/coders/context_coder.py +52 -0
  14. cecli/coders/copypaste_coder.py +269 -0
  15. cecli/coders/editblock_coder.py +656 -0
  16. cecli/coders/editblock_fenced_coder.py +9 -0
  17. cecli/coders/editblock_func_coder.py +140 -0
  18. cecli/coders/editor_diff_fenced_coder.py +8 -0
  19. cecli/coders/editor_editblock_coder.py +8 -0
  20. cecli/coders/editor_whole_coder.py +8 -0
  21. cecli/coders/help_coder.py +15 -0
  22. cecli/coders/patch_coder.py +705 -0
  23. cecli/coders/search_replace.py +757 -0
  24. cecli/coders/shell.py +37 -0
  25. cecli/coders/single_wholefile_func_coder.py +101 -0
  26. cecli/coders/udiff_coder.py +428 -0
  27. cecli/coders/udiff_simple.py +12 -0
  28. cecli/coders/wholefile_coder.py +143 -0
  29. cecli/coders/wholefile_func_coder.py +133 -0
  30. cecli/commands/__init__.py +192 -0
  31. cecli/commands/add.py +226 -0
  32. cecli/commands/agent.py +51 -0
  33. cecli/commands/architect.py +46 -0
  34. cecli/commands/ask.py +44 -0
  35. cecli/commands/chat_mode.py +0 -0
  36. cecli/commands/clear.py +37 -0
  37. cecli/commands/code.py +46 -0
  38. cecli/commands/command_prefix.py +44 -0
  39. cecli/commands/commit.py +52 -0
  40. cecli/commands/context.py +47 -0
  41. cecli/commands/context_blocks.py +124 -0
  42. cecli/commands/context_management.py +51 -0
  43. cecli/commands/copy.py +62 -0
  44. cecli/commands/copy_context.py +81 -0
  45. cecli/commands/core.py +287 -0
  46. cecli/commands/diff.py +68 -0
  47. cecli/commands/drop.py +217 -0
  48. cecli/commands/editor.py +78 -0
  49. cecli/commands/exit.py +55 -0
  50. cecli/commands/git.py +57 -0
  51. cecli/commands/help.py +140 -0
  52. cecli/commands/history_search.py +40 -0
  53. cecli/commands/lint.py +109 -0
  54. cecli/commands/list_sessions.py +56 -0
  55. cecli/commands/load.py +85 -0
  56. cecli/commands/load_session.py +48 -0
  57. cecli/commands/load_skill.py +68 -0
  58. cecli/commands/ls.py +75 -0
  59. cecli/commands/map.py +37 -0
  60. cecli/commands/map_refresh.py +35 -0
  61. cecli/commands/model.py +118 -0
  62. cecli/commands/models.py +41 -0
  63. cecli/commands/multiline_mode.py +38 -0
  64. cecli/commands/paste.py +91 -0
  65. cecli/commands/quit.py +32 -0
  66. cecli/commands/read_only.py +267 -0
  67. cecli/commands/read_only_stub.py +270 -0
  68. cecli/commands/reasoning_effort.py +70 -0
  69. cecli/commands/remove_skill.py +68 -0
  70. cecli/commands/report.py +40 -0
  71. cecli/commands/reset.py +88 -0
  72. cecli/commands/run.py +99 -0
  73. cecli/commands/save.py +49 -0
  74. cecli/commands/save_session.py +43 -0
  75. cecli/commands/settings.py +69 -0
  76. cecli/commands/test.py +58 -0
  77. cecli/commands/think_tokens.py +74 -0
  78. cecli/commands/tokens.py +207 -0
  79. cecli/commands/undo.py +145 -0
  80. cecli/commands/utils/__init__.py +0 -0
  81. cecli/commands/utils/base_command.py +131 -0
  82. cecli/commands/utils/helpers.py +142 -0
  83. cecli/commands/utils/registry.py +53 -0
  84. cecli/commands/utils/save_load_manager.py +98 -0
  85. cecli/commands/voice.py +78 -0
  86. cecli/commands/weak_model.py +123 -0
  87. cecli/commands/web.py +87 -0
  88. cecli/deprecated_args.py +185 -0
  89. cecli/diffs.py +129 -0
  90. cecli/dump.py +29 -0
  91. cecli/editor.py +147 -0
  92. cecli/exceptions.py +115 -0
  93. cecli/format_settings.py +26 -0
  94. cecli/help.py +119 -0
  95. cecli/help_pats.py +19 -0
  96. cecli/helpers/__init__.py +9 -0
  97. cecli/helpers/copypaste.py +123 -0
  98. cecli/helpers/coroutines.py +8 -0
  99. cecli/helpers/file_searcher.py +142 -0
  100. cecli/helpers/model_providers.py +552 -0
  101. cecli/helpers/plugin_manager.py +81 -0
  102. cecli/helpers/profiler.py +162 -0
  103. cecli/helpers/requests.py +77 -0
  104. cecli/helpers/similarity.py +98 -0
  105. cecli/helpers/skills.py +577 -0
  106. cecli/history.py +186 -0
  107. cecli/io.py +1782 -0
  108. cecli/linter.py +304 -0
  109. cecli/llm.py +101 -0
  110. cecli/main.py +1280 -0
  111. cecli/mcp/__init__.py +154 -0
  112. cecli/mcp/oauth.py +250 -0
  113. cecli/mcp/server.py +278 -0
  114. cecli/mdstream.py +243 -0
  115. cecli/models.py +1255 -0
  116. cecli/onboarding.py +301 -0
  117. cecli/prompts/__init__.py +0 -0
  118. cecli/prompts/agent.yml +71 -0
  119. cecli/prompts/architect.yml +35 -0
  120. cecli/prompts/ask.yml +31 -0
  121. cecli/prompts/base.yml +99 -0
  122. cecli/prompts/context.yml +60 -0
  123. cecli/prompts/copypaste.yml +5 -0
  124. cecli/prompts/editblock.yml +143 -0
  125. cecli/prompts/editblock_fenced.yml +106 -0
  126. cecli/prompts/editblock_func.yml +25 -0
  127. cecli/prompts/editor_diff_fenced.yml +115 -0
  128. cecli/prompts/editor_editblock.yml +121 -0
  129. cecli/prompts/editor_whole.yml +46 -0
  130. cecli/prompts/help.yml +37 -0
  131. cecli/prompts/patch.yml +110 -0
  132. cecli/prompts/single_wholefile_func.yml +24 -0
  133. cecli/prompts/udiff.yml +106 -0
  134. cecli/prompts/udiff_simple.yml +13 -0
  135. cecli/prompts/utils/__init__.py +0 -0
  136. cecli/prompts/utils/prompt_registry.py +167 -0
  137. cecli/prompts/utils/system.py +56 -0
  138. cecli/prompts/wholefile.yml +50 -0
  139. cecli/prompts/wholefile_func.yml +24 -0
  140. cecli/queries/tree-sitter-language-pack/README.md +7 -0
  141. cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  142. cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
  143. cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  144. cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
  145. cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
  146. cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
  147. cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
  148. cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  149. cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
  150. cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  151. cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
  152. cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
  153. cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  154. cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
  155. cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
  156. cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
  157. cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
  158. cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  159. cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  160. cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
  161. cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  162. cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  163. cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
  164. cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
  165. cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  166. cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
  167. cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
  168. cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  169. cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
  170. cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  171. cecli/queries/tree-sitter-languages/README.md +24 -0
  172. cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
  173. cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
  174. cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
  175. cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
  176. cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  177. cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
  178. cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
  179. cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
  180. cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
  181. cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
  182. cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  183. cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
  184. cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
  185. cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
  186. cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
  187. cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  188. cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  189. cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
  190. cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
  191. cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
  192. cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
  193. cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
  194. cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
  195. cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
  196. cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
  197. cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
  198. cecli/reasoning_tags.py +82 -0
  199. cecli/repo.py +626 -0
  200. cecli/repomap.py +1368 -0
  201. cecli/report.py +260 -0
  202. cecli/resources/__init__.py +3 -0
  203. cecli/resources/model-metadata.json +25751 -0
  204. cecli/resources/model-settings.yml +2394 -0
  205. cecli/resources/providers.json +67 -0
  206. cecli/run_cmd.py +143 -0
  207. cecli/scrape.py +295 -0
  208. cecli/sendchat.py +250 -0
  209. cecli/sessions.py +281 -0
  210. cecli/special.py +203 -0
  211. cecli/tools/__init__.py +72 -0
  212. cecli/tools/command.py +103 -0
  213. cecli/tools/command_interactive.py +113 -0
  214. cecli/tools/context_manager.py +175 -0
  215. cecli/tools/delete_block.py +154 -0
  216. cecli/tools/delete_line.py +120 -0
  217. cecli/tools/delete_lines.py +144 -0
  218. cecli/tools/extract_lines.py +281 -0
  219. cecli/tools/finished.py +35 -0
  220. cecli/tools/git_branch.py +132 -0
  221. cecli/tools/git_diff.py +49 -0
  222. cecli/tools/git_log.py +43 -0
  223. cecli/tools/git_remote.py +39 -0
  224. cecli/tools/git_show.py +37 -0
  225. cecli/tools/git_status.py +32 -0
  226. cecli/tools/grep.py +242 -0
  227. cecli/tools/indent_lines.py +195 -0
  228. cecli/tools/insert_block.py +263 -0
  229. cecli/tools/list_changes.py +71 -0
  230. cecli/tools/load_skill.py +51 -0
  231. cecli/tools/ls.py +77 -0
  232. cecli/tools/remove_skill.py +51 -0
  233. cecli/tools/replace_all.py +113 -0
  234. cecli/tools/replace_line.py +135 -0
  235. cecli/tools/replace_lines.py +180 -0
  236. cecli/tools/replace_text.py +186 -0
  237. cecli/tools/show_numbered_context.py +137 -0
  238. cecli/tools/thinking.py +52 -0
  239. cecli/tools/undo_change.py +82 -0
  240. cecli/tools/update_todo_list.py +148 -0
  241. cecli/tools/utils/base_tool.py +64 -0
  242. cecli/tools/utils/helpers.py +359 -0
  243. cecli/tools/utils/output.py +119 -0
  244. cecli/tools/utils/registry.py +145 -0
  245. cecli/tools/view_files_matching.py +138 -0
  246. cecli/tools/view_files_with_symbol.py +117 -0
  247. cecli/tui/__init__.py +83 -0
  248. cecli/tui/app.py +971 -0
  249. cecli/tui/io.py +566 -0
  250. cecli/tui/styles.tcss +117 -0
  251. cecli/tui/widgets/__init__.py +19 -0
  252. cecli/tui/widgets/completion_bar.py +331 -0
  253. cecli/tui/widgets/file_list.py +76 -0
  254. cecli/tui/widgets/footer.py +165 -0
  255. cecli/tui/widgets/input_area.py +320 -0
  256. cecli/tui/widgets/key_hints.py +16 -0
  257. cecli/tui/widgets/output.py +354 -0
  258. cecli/tui/widgets/status_bar.py +279 -0
  259. cecli/tui/worker.py +160 -0
  260. cecli/urls.py +16 -0
  261. cecli/utils.py +499 -0
  262. cecli/versioncheck.py +90 -0
  263. cecli/voice.py +90 -0
  264. cecli/waiting.py +38 -0
  265. cecli/watch.py +316 -0
  266. cecli/watch_prompts.py +12 -0
  267. cecli/website/Gemfile +8 -0
  268. cecli/website/_includes/blame.md +162 -0
  269. cecli/website/_includes/get-started.md +22 -0
  270. cecli/website/_includes/help-tip.md +5 -0
  271. cecli/website/_includes/help.md +24 -0
  272. cecli/website/_includes/install.md +5 -0
  273. cecli/website/_includes/keys.md +4 -0
  274. cecli/website/_includes/model-warnings.md +67 -0
  275. cecli/website/_includes/multi-line.md +22 -0
  276. cecli/website/_includes/python-m-aider.md +5 -0
  277. cecli/website/_includes/recording.css +228 -0
  278. cecli/website/_includes/recording.md +34 -0
  279. cecli/website/_includes/replit-pipx.md +9 -0
  280. cecli/website/_includes/works-best.md +1 -0
  281. cecli/website/_sass/custom/custom.scss +103 -0
  282. cecli/website/docs/config/adv-model-settings.md +2498 -0
  283. cecli/website/docs/config/agent-mode.md +320 -0
  284. cecli/website/docs/config/aider_conf.md +548 -0
  285. cecli/website/docs/config/api-keys.md +90 -0
  286. cecli/website/docs/config/custom-commands.md +187 -0
  287. cecli/website/docs/config/dotenv.md +493 -0
  288. cecli/website/docs/config/editor.md +127 -0
  289. cecli/website/docs/config/mcp.md +210 -0
  290. cecli/website/docs/config/model-aliases.md +173 -0
  291. cecli/website/docs/config/options.md +890 -0
  292. cecli/website/docs/config/reasoning.md +210 -0
  293. cecli/website/docs/config/skills.md +172 -0
  294. cecli/website/docs/config/tui.md +126 -0
  295. cecli/website/docs/config.md +44 -0
  296. cecli/website/docs/faq.md +379 -0
  297. cecli/website/docs/git.md +76 -0
  298. cecli/website/docs/index.md +47 -0
  299. cecli/website/docs/install/codespaces.md +39 -0
  300. cecli/website/docs/install/docker.md +48 -0
  301. cecli/website/docs/install/optional.md +100 -0
  302. cecli/website/docs/install/replit.md +8 -0
  303. cecli/website/docs/install.md +115 -0
  304. cecli/website/docs/languages.md +264 -0
  305. cecli/website/docs/legal/contributor-agreement.md +111 -0
  306. cecli/website/docs/legal/privacy.md +104 -0
  307. cecli/website/docs/llms/anthropic.md +77 -0
  308. cecli/website/docs/llms/azure.md +48 -0
  309. cecli/website/docs/llms/bedrock.md +132 -0
  310. cecli/website/docs/llms/cohere.md +34 -0
  311. cecli/website/docs/llms/deepseek.md +32 -0
  312. cecli/website/docs/llms/gemini.md +49 -0
  313. cecli/website/docs/llms/github.md +111 -0
  314. cecli/website/docs/llms/groq.md +36 -0
  315. cecli/website/docs/llms/lm-studio.md +39 -0
  316. cecli/website/docs/llms/ollama.md +75 -0
  317. cecli/website/docs/llms/openai-compat.md +39 -0
  318. cecli/website/docs/llms/openai.md +58 -0
  319. cecli/website/docs/llms/openrouter.md +78 -0
  320. cecli/website/docs/llms/other.md +117 -0
  321. cecli/website/docs/llms/vertex.md +50 -0
  322. cecli/website/docs/llms/warnings.md +10 -0
  323. cecli/website/docs/llms/xai.md +53 -0
  324. cecli/website/docs/llms.md +54 -0
  325. cecli/website/docs/more/analytics.md +127 -0
  326. cecli/website/docs/more/edit-formats.md +116 -0
  327. cecli/website/docs/more/infinite-output.md +192 -0
  328. cecli/website/docs/more-info.md +8 -0
  329. cecli/website/docs/recordings/auto-accept-architect.md +31 -0
  330. cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  331. cecli/website/docs/recordings/index.md +21 -0
  332. cecli/website/docs/recordings/model-accepts-settings.md +69 -0
  333. cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  334. cecli/website/docs/repomap.md +112 -0
  335. cecli/website/docs/scripting.md +100 -0
  336. cecli/website/docs/sessions.md +213 -0
  337. cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
  338. cecli/website/docs/troubleshooting/edit-errors.md +76 -0
  339. cecli/website/docs/troubleshooting/imports.md +62 -0
  340. cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
  341. cecli/website/docs/troubleshooting/support.md +79 -0
  342. cecli/website/docs/troubleshooting/token-limits.md +96 -0
  343. cecli/website/docs/troubleshooting/warnings.md +12 -0
  344. cecli/website/docs/troubleshooting.md +11 -0
  345. cecli/website/docs/usage/browser.md +57 -0
  346. cecli/website/docs/usage/caching.md +49 -0
  347. cecli/website/docs/usage/commands.md +133 -0
  348. cecli/website/docs/usage/conventions.md +119 -0
  349. cecli/website/docs/usage/copypaste.md +136 -0
  350. cecli/website/docs/usage/images-urls.md +48 -0
  351. cecli/website/docs/usage/lint-test.md +118 -0
  352. cecli/website/docs/usage/modes.md +211 -0
  353. cecli/website/docs/usage/not-code.md +179 -0
  354. cecli/website/docs/usage/notifications.md +87 -0
  355. cecli/website/docs/usage/tips.md +79 -0
  356. cecli/website/docs/usage/tutorials.md +30 -0
  357. cecli/website/docs/usage/voice.md +121 -0
  358. cecli/website/docs/usage/watch.md +294 -0
  359. cecli/website/docs/usage.md +102 -0
  360. cecli/website/share/index.md +101 -0
  361. cecli_dev-0.95.5.dist-info/METADATA +549 -0
  362. cecli_dev-0.95.5.dist-info/RECORD +366 -0
  363. cecli_dev-0.95.5.dist-info/WHEEL +5 -0
  364. cecli_dev-0.95.5.dist-info/entry_points.txt +4 -0
  365. cecli_dev-0.95.5.dist-info/licenses/LICENSE.txt +202 -0
  366. cecli_dev-0.95.5.dist-info/top_level.txt +1 -0
cecli/repo.py ADDED
@@ -0,0 +1,626 @@
1
+ import contextlib
2
+ import os
3
+ import time
4
+ from pathlib import Path, PurePosixPath
5
+
6
+ try:
7
+ import git
8
+
9
+ ANY_GIT_ERROR = [
10
+ git.exc.ODBError,
11
+ git.exc.GitError,
12
+ git.exc.InvalidGitRepositoryError,
13
+ git.exc.GitCommandNotFound,
14
+ ]
15
+ except ImportError:
16
+ git = None
17
+ ANY_GIT_ERROR = []
18
+
19
+ import pathspec
20
+
21
+ import cecli.prompts.utils.system as prompts
22
+ from cecli import utils
23
+
24
+ from .dump import dump # noqa: F401
25
+
26
+ ANY_GIT_ERROR += [
27
+ OSError,
28
+ IndexError,
29
+ BufferError,
30
+ TypeError,
31
+ ValueError,
32
+ AttributeError,
33
+ AssertionError,
34
+ TimeoutError,
35
+ ]
36
+ ANY_GIT_ERROR = tuple(ANY_GIT_ERROR)
37
+
38
+
39
+ @contextlib.contextmanager
40
+ def set_git_env(var_name, value, original_value):
41
+ """Temporarily set a Git environment variable."""
42
+ os.environ[var_name] = value
43
+ try:
44
+ yield
45
+ finally:
46
+ if original_value is not None:
47
+ os.environ[var_name] = original_value
48
+ elif var_name in os.environ:
49
+ del os.environ[var_name]
50
+
51
+
52
+ class GitRepo:
53
+ repo = None
54
+ cecli_ignore_file = None
55
+ cecli_ignore_spec = None
56
+ cecli_ignore_ts = 0
57
+ cecli_ignore_last_check = 0
58
+ subtree_only = False
59
+ ignore_file_cache = {}
60
+ git_repo_error = None
61
+
62
+ def __init__(
63
+ self,
64
+ io,
65
+ fnames,
66
+ git_dname,
67
+ cecli_ignore_file=None,
68
+ models=None,
69
+ attribute_author=True,
70
+ attribute_committer=True,
71
+ attribute_commit_message_author=False,
72
+ attribute_commit_message_committer=False,
73
+ commit_prompt=None,
74
+ subtree_only=False,
75
+ git_commit_verify=True,
76
+ attribute_co_authored_by=False, # Added parameter
77
+ ):
78
+ self.io = io
79
+ self.models = models
80
+
81
+ self.normalized_path = {}
82
+ self.tree_files = {}
83
+
84
+ self.attribute_author = attribute_author
85
+ self.attribute_committer = attribute_committer
86
+ self.attribute_commit_message_author = attribute_commit_message_author
87
+ self.attribute_commit_message_committer = attribute_commit_message_committer
88
+ self.attribute_co_authored_by = attribute_co_authored_by # Assign from parameter
89
+ self.commit_prompt = commit_prompt
90
+ self.subtree_only = subtree_only
91
+ self.git_commit_verify = git_commit_verify
92
+ self.ignore_file_cache = {}
93
+
94
+ if git_dname:
95
+ check_fnames = [git_dname]
96
+ elif fnames:
97
+ check_fnames = fnames
98
+ else:
99
+ check_fnames = ["."]
100
+
101
+ repo_paths = []
102
+ for fname in check_fnames:
103
+ fname = Path(fname)
104
+ fname = fname.resolve()
105
+
106
+ if not fname.exists() and fname.parent.exists():
107
+ fname = fname.parent
108
+
109
+ try:
110
+ repo_path = git.Repo(fname, search_parent_directories=True).working_dir
111
+ repo_path = utils.safe_abs_path(repo_path)
112
+ repo_paths.append(repo_path)
113
+ except ANY_GIT_ERROR:
114
+ pass
115
+
116
+ num_repos = len(set(repo_paths))
117
+
118
+ if num_repos == 0:
119
+ raise FileNotFoundError
120
+ if num_repos > 1:
121
+ self.io.tool_error("Files are in different git repos.")
122
+ raise FileNotFoundError
123
+
124
+ # https://github.com/gitpython-developers/GitPython/issues/427
125
+ self.repo = git.Repo(repo_paths.pop(), odbt=git.GitDB)
126
+ self.root = utils.safe_abs_path(self.repo.working_tree_dir)
127
+
128
+ if cecli_ignore_file:
129
+ self.cecli_ignore_file = Path(cecli_ignore_file)
130
+
131
+ async def commit(self, fnames=None, context=None, message=None, coder_edits=False, coder=None):
132
+ """
133
+ Commit the specified files or all dirty files if none are specified.
134
+
135
+ Args:
136
+ fnames (list, optional): List of filenames to commit. Defaults to None (commit all
137
+ dirty files).
138
+ context (str, optional): Context for generating commit message. Defaults to None.
139
+ message (str, optional): Explicit commit message. Defaults to None (generate message).
140
+ coder_edits (bool, optional): Whether the changes were made by cecli. Defaults to False.
141
+ This affects attribution logic.
142
+ coder (Coder, optional): The Coder instance, used for config and model info.
143
+ Defaults to None.
144
+
145
+ Returns:
146
+ tuple(str, str) or None: The commit hash and commit message if successful,
147
+ else None.
148
+
149
+ Attribution Logic:
150
+ ------------------
151
+ This method handles Git commit attribution based on configuration flags and whether
152
+ cecli generated the changes (`coder_edits`).
153
+
154
+ Key Concepts:
155
+ - Author: The person who originally wrote the code changes.
156
+ - Committer: The person who last applied the commit to the repository.
157
+ - coder_edits=True: Changes were generated by cecli (LLM).
158
+ - coder_edits=False: Commit is user-driven (e.g., /commit manually staged changes).
159
+ - Explicit Setting: A flag (--attribute-...) is set to True or False
160
+ via command line or config file.
161
+ - Implicit Default: A flag is not explicitly set, defaulting to None in args, which is
162
+ interpreted as True unless overridden by other logic.
163
+
164
+ Flags:
165
+ - --attribute-author: Modify Author name to "User Name (cecli)".
166
+ - --attribute-committer: Modify Committer name to "User Name (cecli)".
167
+ - --attribute-co-authored-by: Add
168
+ "Co-authored-by: cecli (<model>)" trailer to commit message.
169
+
170
+ Behavior Summary:
171
+
172
+ 1. When coder_edits = True (AI Changes):
173
+ - If --attribute-co-authored-by=True:
174
+ - Co-authored-by trailer IS ADDED.
175
+ - Author/Committer names are NOT modified by default (co-authored-by takes precedence).
176
+ - EXCEPTION: If --attribute-author/--attribute-committer is EXPLICITLY True, the
177
+ respective name IS modified (explicit overrides precedence).
178
+ - If --attribute-co-authored-by=False:
179
+ - Co-authored-by trailer is NOT added.
180
+ - Author/Committer names ARE modified by default (implicit True).
181
+ - EXCEPTION: If --attribute-author/--attribute-committer is EXPLICITLY False,
182
+ the respective name is NOT modified.
183
+
184
+ 2. When coder_edits = False (User Changes):
185
+ - --attribute-co-authored-by is IGNORED (trailer never added).
186
+ - Author name is NEVER modified (--attribute-author ignored).
187
+ - Committer name IS modified by default (implicit True, as cecli runs `git commit`).
188
+ - EXCEPTION: If --attribute-committer is EXPLICITLY False, the name is NOT modified.
189
+
190
+ Resulting Scenarios:
191
+ - Standard AI edit (defaults): Co-authored-by=False -> Author=You(cecli),
192
+ Committer=You(cecli)
193
+ - AI edit with Co-authored-by (default): Co-authored-by=True -> Author=You,
194
+ Committer=You, Trailer added
195
+ - AI edit with Co-authored-by + Explicit Author: Co-authored-by=True,
196
+ --attribute-author -> Author=You(cecli), Committer=You, Trailer added
197
+ - User commit (defaults): coder_edits=False -> Author=You, Committer=You(cecli)
198
+ - User commit with explicit no-committer: coder_edits=False,
199
+ --no-attribute-committer -> Author=You, Committer=You
200
+ """
201
+ if not fnames and not self.repo.is_dirty():
202
+ return
203
+
204
+ diffs = self.get_diffs(fnames)
205
+ if not diffs:
206
+ return
207
+
208
+ if message:
209
+ commit_message = message
210
+ else:
211
+ user_language = None
212
+ if coder:
213
+ user_language = coder.commit_language
214
+ if not user_language:
215
+ user_language = coder.get_user_language()
216
+ commit_message = await self.get_commit_message(diffs, context, user_language)
217
+
218
+ # Retrieve attribute settings, prioritizing coder.args if available
219
+ if coder and hasattr(coder, "args") and coder.args:
220
+ attribute_author = coder.args.attribute_author
221
+ attribute_committer = coder.args.attribute_committer
222
+ attribute_commit_message_author = coder.args.attribute_commit_message_author
223
+ attribute_commit_message_committer = coder.args.attribute_commit_message_committer
224
+ attribute_co_authored_by = coder.args.attribute_co_authored_by
225
+ else:
226
+ # Fallback to self attributes (initialized from config/defaults)
227
+ attribute_author = self.attribute_author
228
+ attribute_committer = self.attribute_committer
229
+ attribute_commit_message_author = self.attribute_commit_message_author
230
+ attribute_commit_message_committer = self.attribute_commit_message_committer
231
+ attribute_co_authored_by = self.attribute_co_authored_by
232
+
233
+ # Determine explicit settings (None means use default behavior)
234
+ author_explicit = attribute_author is not None
235
+ committer_explicit = attribute_committer is not None
236
+
237
+ # Determine effective settings (apply default True if not explicit)
238
+ effective_author = True if attribute_author is None else attribute_author
239
+ effective_committer = True if attribute_committer is None else attribute_committer
240
+
241
+ # Determine commit message prefixing
242
+ prefix_commit_message = coder_edits and (
243
+ attribute_commit_message_author or attribute_commit_message_committer
244
+ )
245
+
246
+ # Determine Co-authored-by trailer
247
+ commit_message_trailer = ""
248
+ if coder_edits and attribute_co_authored_by:
249
+ model_name = "unknown-model"
250
+ if coder and hasattr(coder, "main_model") and coder.main_model.name:
251
+ model_name = coder.main_model.name
252
+ commit_message_trailer = f"\n\nCo-authored-by: cecli ({model_name})"
253
+
254
+ # Determine if author/committer names should be modified
255
+ # Author modification applies only to cecli edits.
256
+ # It's used if effective_author is True AND
257
+ # (co-authored-by is False OR author was explicitly set).
258
+ use_attribute_author = (
259
+ coder_edits and effective_author and (not attribute_co_authored_by or author_explicit)
260
+ )
261
+
262
+ # Committer modification applies regardless of coder_edits (based on tests).
263
+ # It's used if effective_committer is True AND
264
+ # (it's not an cecli edit with co-authored-by OR committer was explicitly set).
265
+ use_attribute_committer = effective_committer and (
266
+ not (coder_edits and attribute_co_authored_by) or committer_explicit
267
+ )
268
+
269
+ if not commit_message:
270
+ commit_message = "(no commit message provided)"
271
+
272
+ if prefix_commit_message:
273
+ commit_message = "cecli: " + commit_message
274
+
275
+ full_commit_message = commit_message + commit_message_trailer
276
+
277
+ cmd = ["-m", full_commit_message]
278
+ if not self.git_commit_verify:
279
+ cmd.append("--no-verify")
280
+ if fnames:
281
+ fnames = [str(self.abs_root_path(fn)) for fn in fnames]
282
+ for fname in fnames:
283
+ try:
284
+ self.repo.git.add(fname)
285
+ except ANY_GIT_ERROR as err:
286
+ self.io.tool_error(f"Unable to add {fname}: {err}")
287
+ cmd += ["--"] + fnames
288
+ else:
289
+ cmd += ["-a"]
290
+
291
+ original_user_name = self.repo.git.config("--get", "user.name")
292
+ original_committer_name_env = os.environ.get("GIT_COMMITTER_NAME")
293
+ original_author_name_env = os.environ.get("GIT_AUTHOR_NAME")
294
+ committer_name = f"{original_user_name} (cecli)"
295
+
296
+ try:
297
+ # Use context managers to handle environment variables
298
+ with contextlib.ExitStack() as stack:
299
+ if use_attribute_committer:
300
+ stack.enter_context(
301
+ set_git_env(
302
+ "GIT_COMMITTER_NAME", committer_name, original_committer_name_env
303
+ )
304
+ )
305
+ if use_attribute_author:
306
+ stack.enter_context(
307
+ set_git_env("GIT_AUTHOR_NAME", committer_name, original_author_name_env)
308
+ )
309
+
310
+ # Perform the commit
311
+ self.repo.git.commit(cmd)
312
+ commit_hash = self.get_head_commit_sha(short=True)
313
+ self.io.tool_success(f"Commit {commit_hash} {commit_message}")
314
+ return commit_hash, commit_message
315
+
316
+ except ANY_GIT_ERROR as err:
317
+ self.io.tool_error(f"Unable to commit: {err}")
318
+ # No return here, implicitly returns None
319
+
320
+ def get_rel_repo_dir(self):
321
+ try:
322
+ return os.path.relpath(self.repo.git_dir, os.getcwd())
323
+ except (ValueError, OSError):
324
+ return self.repo.git_dir
325
+
326
+ async def get_commit_message(self, diffs, context, user_language=None):
327
+ diffs = "# Diffs:\n" + diffs
328
+
329
+ content = ""
330
+ if context:
331
+ content += context + "\n"
332
+ content += diffs
333
+
334
+ system_content = self.commit_prompt or prompts.commit_system
335
+
336
+ language_instruction = ""
337
+ if user_language:
338
+ language_instruction = f"\n- Is written in {user_language}."
339
+ system_content = system_content.format(language_instruction=language_instruction)
340
+
341
+ commit_message = None
342
+ for model in self.models:
343
+ spinner_text = f"Generating commit message with {model.name}\n"
344
+ self.io.start_spinner(spinner_text, update_last_text=False)
345
+
346
+ if model.system_prompt_prefix:
347
+ current_system_content = model.system_prompt_prefix + "\n" + system_content
348
+ else:
349
+ current_system_content = system_content
350
+
351
+ messages = [
352
+ dict(role="system", content=current_system_content),
353
+ dict(role="user", content=content),
354
+ ]
355
+
356
+ num_tokens = model.token_count(messages)
357
+ max_tokens = model.info.get("max_input_tokens") or 0
358
+
359
+ if max_tokens and num_tokens > max_tokens:
360
+ continue
361
+
362
+ commit_message = await model.simple_send_with_retries(messages)
363
+ if commit_message:
364
+ break # Found a model that could generate the message
365
+
366
+ if not commit_message:
367
+ self.io.tool_error("Failed to generate commit message!")
368
+ return
369
+
370
+ commit_message = commit_message.strip()
371
+ if commit_message and commit_message[0] == '"' and commit_message[-1] == '"':
372
+ commit_message = commit_message[1:-1].strip()
373
+
374
+ self.io.start_spinner(self.io.last_spinner_text, update_last_text=False)
375
+ return commit_message
376
+
377
+ def get_diffs(self, fnames=None):
378
+ # We always want diffs of index and working dir
379
+
380
+ current_branch_has_commits = False
381
+ try:
382
+ active_branch = self.repo.active_branch
383
+ try:
384
+ commits = self.repo.iter_commits(active_branch)
385
+ current_branch_has_commits = any(commits)
386
+ except ANY_GIT_ERROR:
387
+ pass
388
+ except (TypeError,) + ANY_GIT_ERROR:
389
+ pass
390
+
391
+ if not fnames:
392
+ fnames = []
393
+
394
+ diffs = ""
395
+ for fname in fnames:
396
+ if not self.path_in_repo(fname):
397
+ diffs += f"Added {fname}\n"
398
+
399
+ try:
400
+ if current_branch_has_commits:
401
+ args = ["HEAD", "--"] + list(fnames)
402
+ diffs += self.repo.git.diff(*args, stdout_as_string=False).decode(
403
+ self.io.encoding, "replace"
404
+ )
405
+ return diffs
406
+
407
+ wd_args = ["--"] + list(fnames)
408
+ index_args = ["--cached"] + wd_args
409
+
410
+ diffs += self.repo.git.diff(*index_args, stdout_as_string=False).decode(
411
+ self.io.encoding, "replace"
412
+ )
413
+ diffs += self.repo.git.diff(*wd_args, stdout_as_string=False).decode(
414
+ self.io.encoding, "replace"
415
+ )
416
+
417
+ return diffs
418
+ except ANY_GIT_ERROR as err:
419
+ self.io.tool_error(f"Unable to diff: {err}")
420
+
421
+ def diff_commits(self, pretty, from_commit, to_commit=None):
422
+ args = []
423
+ if pretty:
424
+ args += ["--color"]
425
+ else:
426
+ args += ["--color=never"]
427
+
428
+ if to_commit is not None:
429
+ args += [from_commit, to_commit]
430
+ else:
431
+ args += [from_commit]
432
+ diffs = self.repo.git.diff(*args, stdout_as_string=False).decode(
433
+ self.io.encoding, "replace"
434
+ )
435
+
436
+ return diffs
437
+
438
+ def get_tracked_files(self):
439
+ if not self.repo:
440
+ return []
441
+
442
+ try:
443
+ commit = self.repo.head.commit
444
+ except ValueError:
445
+ commit = None
446
+ except ANY_GIT_ERROR as err:
447
+ self.git_repo_error = err
448
+ self.io.tool_error(f"Unable to list files in git repo: {err}")
449
+ self.io.tool_output("Is your git repo corrupted?")
450
+ return []
451
+
452
+ files = set()
453
+ if commit:
454
+ if commit in self.tree_files:
455
+ files = self.tree_files[commit]
456
+ else:
457
+ try:
458
+ iterator = commit.tree.traverse()
459
+ blob = None # Initialize blob
460
+ while True:
461
+ try:
462
+ blob = next(iterator)
463
+ if blob.type == "blob": # blob is a file
464
+ files.add(blob.path)
465
+ except IndexError:
466
+ # Handle potential index error during tree traversal
467
+ # without relying on potentially unassigned 'blob'
468
+ self.io.tool_warning(
469
+ "GitRepo: Index error encountered while reading git tree object."
470
+ " Skipping."
471
+ )
472
+ continue
473
+ except StopIteration:
474
+ break
475
+ except ANY_GIT_ERROR as err:
476
+ self.git_repo_error = err
477
+ self.io.tool_error(f"Unable to list files in git repo: {err}")
478
+ self.io.tool_output("Is your git repo corrupted?")
479
+ return []
480
+ files = set(self.normalize_path(path) for path in files)
481
+ self.tree_files[commit] = set(files)
482
+
483
+ # Add staged files
484
+ index = self.repo.index
485
+ try:
486
+ staged_files = [path for path, _ in index.entries.keys()]
487
+ files.update(self.normalize_path(path) for path in staged_files)
488
+ except ANY_GIT_ERROR as err:
489
+ self.io.tool_error(f"Unable to read staged files: {err}")
490
+
491
+ res = [fname for fname in files if not self.ignored_file(fname)]
492
+
493
+ return res
494
+
495
+ def normalize_path(self, path):
496
+ orig_path = path
497
+ res = self.normalized_path.get(orig_path)
498
+ if res:
499
+ return res
500
+
501
+ path = str(Path(PurePosixPath((Path(self.root) / path).relative_to(self.root))))
502
+ self.normalized_path[orig_path] = path
503
+ return path
504
+
505
+ def refresh_cecli_ignore(self):
506
+ if not self.cecli_ignore_file:
507
+ return
508
+
509
+ current_time = time.time()
510
+ if current_time - self.cecli_ignore_last_check < 1:
511
+ return
512
+
513
+ self.cecli_ignore_last_check = current_time
514
+
515
+ if not self.cecli_ignore_file.is_file():
516
+ return
517
+
518
+ mtime = self.cecli_ignore_file.stat().st_mtime
519
+ if mtime != self.cecli_ignore_ts:
520
+ self.cecli_ignore_ts = mtime
521
+ self.ignore_file_cache = {}
522
+ lines = self.cecli_ignore_file.read_text().splitlines()
523
+ self.cecli_ignore_spec = pathspec.PathSpec.from_lines(
524
+ pathspec.patterns.GitWildMatchPattern,
525
+ lines,
526
+ )
527
+
528
+ def git_ignored_file(self, path):
529
+ if not self.repo:
530
+ return
531
+ try:
532
+ if self.repo.ignored(path):
533
+ return True
534
+ except ANY_GIT_ERROR:
535
+ return False
536
+
537
+ def ignored_file(self, fname):
538
+ self.refresh_cecli_ignore()
539
+
540
+ if fname in self.ignore_file_cache:
541
+ return self.ignore_file_cache[fname]
542
+
543
+ result = self.ignored_file_raw(fname)
544
+ self.ignore_file_cache[fname] = result
545
+ return result
546
+
547
+ def ignored_file_raw(self, fname):
548
+ if self.subtree_only:
549
+ try:
550
+ fname_path = Path(self.normalize_path(fname))
551
+ cwd_path = Path.cwd().resolve().relative_to(Path(self.root).resolve())
552
+ except ValueError:
553
+ # Issue #1524
554
+ # ValueError: 'C:\\dev\\squid-certbot' is not in the subpath of
555
+ # 'C:\\dev\\squid-certbot'
556
+ # Clearly, fname is not under cwd... so ignore it
557
+ return True
558
+
559
+ if cwd_path not in fname_path.parents and fname_path != cwd_path:
560
+ return True
561
+
562
+ if not self.cecli_ignore_file or not self.cecli_ignore_file.is_file():
563
+ return False
564
+
565
+ try:
566
+ fname = self.normalize_path(fname)
567
+ except ValueError:
568
+ return True
569
+
570
+ return self.cecli_ignore_spec.match_file(fname)
571
+
572
+ def path_in_repo(self, path):
573
+ if not self.repo:
574
+ return
575
+ if not path:
576
+ return
577
+
578
+ tracked_files = set(self.get_tracked_files())
579
+ return self.normalize_path(path) in tracked_files
580
+
581
+ def abs_root_path(self, path):
582
+ res = Path(self.root) / path
583
+ return utils.safe_abs_path(res)
584
+
585
+ def get_dirty_files(self):
586
+ """
587
+ Returns a list of all files which are dirty (not committed), either staged or in the working
588
+ directory.
589
+ """
590
+ dirty_files = set()
591
+
592
+ # Get staged files
593
+ staged_files = self.repo.git.diff("--name-only", "--cached").splitlines()
594
+ dirty_files.update(staged_files)
595
+
596
+ # Get unstaged files
597
+ unstaged_files = self.repo.git.diff("--name-only").splitlines()
598
+ dirty_files.update(unstaged_files)
599
+
600
+ return list(dirty_files)
601
+
602
+ def is_dirty(self, path=None):
603
+ if path and not self.path_in_repo(path):
604
+ return True
605
+
606
+ return self.repo.is_dirty(path=path)
607
+
608
+ def get_head_commit(self):
609
+ try:
610
+ return self.repo.head.commit
611
+ except (ValueError,) + ANY_GIT_ERROR:
612
+ return None
613
+
614
+ def get_head_commit_sha(self, short=False):
615
+ commit = self.get_head_commit()
616
+ if not commit:
617
+ return
618
+ if short:
619
+ return commit.hexsha[:7]
620
+ return commit.hexsha
621
+
622
+ def get_head_commit_message(self, default=None):
623
+ commit = self.get_head_commit()
624
+ if not commit:
625
+ return default
626
+ return commit.message