aider-ce 0.88.20__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 (279) hide show
  1. aider/__init__.py +20 -0
  2. aider/__main__.py +4 -0
  3. aider/_version.py +34 -0
  4. aider/analytics.py +258 -0
  5. aider/args.py +1056 -0
  6. aider/args_formatter.py +228 -0
  7. aider/change_tracker.py +133 -0
  8. aider/coders/__init__.py +36 -0
  9. aider/coders/agent_coder.py +2166 -0
  10. aider/coders/agent_prompts.py +104 -0
  11. aider/coders/architect_coder.py +48 -0
  12. aider/coders/architect_prompts.py +40 -0
  13. aider/coders/ask_coder.py +9 -0
  14. aider/coders/ask_prompts.py +35 -0
  15. aider/coders/base_coder.py +3613 -0
  16. aider/coders/base_prompts.py +87 -0
  17. aider/coders/chat_chunks.py +64 -0
  18. aider/coders/context_coder.py +53 -0
  19. aider/coders/context_prompts.py +75 -0
  20. aider/coders/editblock_coder.py +657 -0
  21. aider/coders/editblock_fenced_coder.py +10 -0
  22. aider/coders/editblock_fenced_prompts.py +143 -0
  23. aider/coders/editblock_func_coder.py +141 -0
  24. aider/coders/editblock_func_prompts.py +27 -0
  25. aider/coders/editblock_prompts.py +175 -0
  26. aider/coders/editor_diff_fenced_coder.py +9 -0
  27. aider/coders/editor_diff_fenced_prompts.py +11 -0
  28. aider/coders/editor_editblock_coder.py +9 -0
  29. aider/coders/editor_editblock_prompts.py +21 -0
  30. aider/coders/editor_whole_coder.py +9 -0
  31. aider/coders/editor_whole_prompts.py +12 -0
  32. aider/coders/help_coder.py +16 -0
  33. aider/coders/help_prompts.py +46 -0
  34. aider/coders/patch_coder.py +706 -0
  35. aider/coders/patch_prompts.py +159 -0
  36. aider/coders/search_replace.py +757 -0
  37. aider/coders/shell.py +37 -0
  38. aider/coders/single_wholefile_func_coder.py +102 -0
  39. aider/coders/single_wholefile_func_prompts.py +27 -0
  40. aider/coders/udiff_coder.py +429 -0
  41. aider/coders/udiff_prompts.py +115 -0
  42. aider/coders/udiff_simple.py +14 -0
  43. aider/coders/udiff_simple_prompts.py +25 -0
  44. aider/coders/wholefile_coder.py +144 -0
  45. aider/coders/wholefile_func_coder.py +134 -0
  46. aider/coders/wholefile_func_prompts.py +27 -0
  47. aider/coders/wholefile_prompts.py +65 -0
  48. aider/commands.py +2173 -0
  49. aider/copypaste.py +72 -0
  50. aider/deprecated.py +126 -0
  51. aider/diffs.py +128 -0
  52. aider/dump.py +29 -0
  53. aider/editor.py +147 -0
  54. aider/exceptions.py +115 -0
  55. aider/format_settings.py +26 -0
  56. aider/gui.py +545 -0
  57. aider/help.py +163 -0
  58. aider/help_pats.py +19 -0
  59. aider/helpers/__init__.py +9 -0
  60. aider/helpers/similarity.py +98 -0
  61. aider/history.py +180 -0
  62. aider/io.py +1608 -0
  63. aider/linter.py +304 -0
  64. aider/llm.py +55 -0
  65. aider/main.py +1415 -0
  66. aider/mcp/__init__.py +174 -0
  67. aider/mcp/server.py +149 -0
  68. aider/mdstream.py +243 -0
  69. aider/models.py +1313 -0
  70. aider/onboarding.py +429 -0
  71. aider/openrouter.py +129 -0
  72. aider/prompts.py +56 -0
  73. aider/queries/tree-sitter-language-pack/README.md +7 -0
  74. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  78. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  79. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  80. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  81. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  82. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  83. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  84. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  85. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  86. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  87. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  88. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  89. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  90. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  91. aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  92. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  93. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  94. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  95. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  96. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  97. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  98. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  99. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  100. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  101. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  102. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  103. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  104. aider/queries/tree-sitter-languages/README.md +24 -0
  105. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  106. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  107. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  108. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  109. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  110. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  111. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  112. aider/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  113. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  114. aider/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  115. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  116. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  117. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  118. aider/queries/tree-sitter-languages/julia-tags.scm +60 -0
  119. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  120. aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  121. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  122. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  123. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  124. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  125. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  126. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  127. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  128. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  129. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  130. aider/queries/tree-sitter-languages/zig-tags.scm +3 -0
  131. aider/reasoning_tags.py +82 -0
  132. aider/repo.py +621 -0
  133. aider/repomap.py +1174 -0
  134. aider/report.py +260 -0
  135. aider/resources/__init__.py +3 -0
  136. aider/resources/model-metadata.json +776 -0
  137. aider/resources/model-settings.yml +2068 -0
  138. aider/run_cmd.py +133 -0
  139. aider/scrape.py +293 -0
  140. aider/sendchat.py +242 -0
  141. aider/sessions.py +256 -0
  142. aider/special.py +203 -0
  143. aider/tools/__init__.py +72 -0
  144. aider/tools/command.py +105 -0
  145. aider/tools/command_interactive.py +122 -0
  146. aider/tools/delete_block.py +182 -0
  147. aider/tools/delete_line.py +155 -0
  148. aider/tools/delete_lines.py +184 -0
  149. aider/tools/extract_lines.py +341 -0
  150. aider/tools/finished.py +48 -0
  151. aider/tools/git_branch.py +129 -0
  152. aider/tools/git_diff.py +60 -0
  153. aider/tools/git_log.py +57 -0
  154. aider/tools/git_remote.py +53 -0
  155. aider/tools/git_show.py +51 -0
  156. aider/tools/git_status.py +46 -0
  157. aider/tools/grep.py +256 -0
  158. aider/tools/indent_lines.py +221 -0
  159. aider/tools/insert_block.py +288 -0
  160. aider/tools/list_changes.py +86 -0
  161. aider/tools/ls.py +93 -0
  162. aider/tools/make_editable.py +85 -0
  163. aider/tools/make_readonly.py +69 -0
  164. aider/tools/remove.py +91 -0
  165. aider/tools/replace_all.py +126 -0
  166. aider/tools/replace_line.py +173 -0
  167. aider/tools/replace_lines.py +217 -0
  168. aider/tools/replace_text.py +187 -0
  169. aider/tools/show_numbered_context.py +147 -0
  170. aider/tools/tool_utils.py +313 -0
  171. aider/tools/undo_change.py +95 -0
  172. aider/tools/update_todo_list.py +156 -0
  173. aider/tools/view.py +57 -0
  174. aider/tools/view_files_matching.py +141 -0
  175. aider/tools/view_files_with_symbol.py +129 -0
  176. aider/urls.py +17 -0
  177. aider/utils.py +456 -0
  178. aider/versioncheck.py +113 -0
  179. aider/voice.py +205 -0
  180. aider/waiting.py +38 -0
  181. aider/watch.py +318 -0
  182. aider/watch_prompts.py +12 -0
  183. aider/website/Gemfile +8 -0
  184. aider/website/_includes/blame.md +162 -0
  185. aider/website/_includes/get-started.md +22 -0
  186. aider/website/_includes/help-tip.md +5 -0
  187. aider/website/_includes/help.md +24 -0
  188. aider/website/_includes/install.md +5 -0
  189. aider/website/_includes/keys.md +4 -0
  190. aider/website/_includes/model-warnings.md +67 -0
  191. aider/website/_includes/multi-line.md +22 -0
  192. aider/website/_includes/python-m-aider.md +5 -0
  193. aider/website/_includes/recording.css +228 -0
  194. aider/website/_includes/recording.md +34 -0
  195. aider/website/_includes/replit-pipx.md +9 -0
  196. aider/website/_includes/works-best.md +1 -0
  197. aider/website/_sass/custom/custom.scss +103 -0
  198. aider/website/docs/config/adv-model-settings.md +2261 -0
  199. aider/website/docs/config/agent-mode.md +194 -0
  200. aider/website/docs/config/aider_conf.md +548 -0
  201. aider/website/docs/config/api-keys.md +90 -0
  202. aider/website/docs/config/dotenv.md +493 -0
  203. aider/website/docs/config/editor.md +127 -0
  204. aider/website/docs/config/mcp.md +95 -0
  205. aider/website/docs/config/model-aliases.md +104 -0
  206. aider/website/docs/config/options.md +890 -0
  207. aider/website/docs/config/reasoning.md +210 -0
  208. aider/website/docs/config.md +44 -0
  209. aider/website/docs/faq.md +384 -0
  210. aider/website/docs/git.md +76 -0
  211. aider/website/docs/index.md +47 -0
  212. aider/website/docs/install/codespaces.md +39 -0
  213. aider/website/docs/install/docker.md +57 -0
  214. aider/website/docs/install/optional.md +100 -0
  215. aider/website/docs/install/replit.md +8 -0
  216. aider/website/docs/install.md +115 -0
  217. aider/website/docs/languages.md +264 -0
  218. aider/website/docs/legal/contributor-agreement.md +111 -0
  219. aider/website/docs/legal/privacy.md +104 -0
  220. aider/website/docs/llms/anthropic.md +77 -0
  221. aider/website/docs/llms/azure.md +48 -0
  222. aider/website/docs/llms/bedrock.md +132 -0
  223. aider/website/docs/llms/cohere.md +34 -0
  224. aider/website/docs/llms/deepseek.md +32 -0
  225. aider/website/docs/llms/gemini.md +49 -0
  226. aider/website/docs/llms/github.md +111 -0
  227. aider/website/docs/llms/groq.md +36 -0
  228. aider/website/docs/llms/lm-studio.md +39 -0
  229. aider/website/docs/llms/ollama.md +75 -0
  230. aider/website/docs/llms/openai-compat.md +39 -0
  231. aider/website/docs/llms/openai.md +58 -0
  232. aider/website/docs/llms/openrouter.md +78 -0
  233. aider/website/docs/llms/other.md +117 -0
  234. aider/website/docs/llms/vertex.md +50 -0
  235. aider/website/docs/llms/warnings.md +10 -0
  236. aider/website/docs/llms/xai.md +53 -0
  237. aider/website/docs/llms.md +54 -0
  238. aider/website/docs/more/analytics.md +127 -0
  239. aider/website/docs/more/edit-formats.md +116 -0
  240. aider/website/docs/more/infinite-output.md +165 -0
  241. aider/website/docs/more-info.md +8 -0
  242. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  243. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  244. aider/website/docs/recordings/index.md +21 -0
  245. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  246. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  247. aider/website/docs/repomap.md +112 -0
  248. aider/website/docs/scripting.md +100 -0
  249. aider/website/docs/sessions.md +203 -0
  250. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  251. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  252. aider/website/docs/troubleshooting/imports.md +62 -0
  253. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  254. aider/website/docs/troubleshooting/support.md +79 -0
  255. aider/website/docs/troubleshooting/token-limits.md +96 -0
  256. aider/website/docs/troubleshooting/warnings.md +12 -0
  257. aider/website/docs/troubleshooting.md +11 -0
  258. aider/website/docs/usage/browser.md +57 -0
  259. aider/website/docs/usage/caching.md +49 -0
  260. aider/website/docs/usage/commands.md +133 -0
  261. aider/website/docs/usage/conventions.md +119 -0
  262. aider/website/docs/usage/copypaste.md +121 -0
  263. aider/website/docs/usage/images-urls.md +48 -0
  264. aider/website/docs/usage/lint-test.md +118 -0
  265. aider/website/docs/usage/modes.md +211 -0
  266. aider/website/docs/usage/not-code.md +179 -0
  267. aider/website/docs/usage/notifications.md +87 -0
  268. aider/website/docs/usage/tips.md +79 -0
  269. aider/website/docs/usage/tutorials.md +30 -0
  270. aider/website/docs/usage/voice.md +121 -0
  271. aider/website/docs/usage/watch.md +294 -0
  272. aider/website/docs/usage.md +102 -0
  273. aider/website/share/index.md +101 -0
  274. aider_ce-0.88.20.dist-info/METADATA +187 -0
  275. aider_ce-0.88.20.dist-info/RECORD +279 -0
  276. aider_ce-0.88.20.dist-info/WHEEL +5 -0
  277. aider_ce-0.88.20.dist-info/entry_points.txt +2 -0
  278. aider_ce-0.88.20.dist-info/licenses/LICENSE.txt +202 -0
  279. aider_ce-0.88.20.dist-info/top_level.txt +1 -0
aider/repo.py ADDED
@@ -0,0 +1,621 @@
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
+ from aider import prompts, utils
22
+
23
+ from .dump import dump # noqa: F401
24
+ from .waiting import Spinner
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
+ aider_ignore_file = None
55
+ aider_ignore_spec = None
56
+ aider_ignore_ts = 0
57
+ aider_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
+ aider_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 aider_ignore_file:
129
+ self.aider_ignore_file = Path(aider_ignore_file)
130
+
131
+ async def commit(self, fnames=None, context=None, message=None, aider_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
+ aider_edits (bool, optional): Whether the changes were made by Aider. 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
+ Aider generated the changes (`aider_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
+ - aider_edits=True: Changes were generated by Aider (LLM).
158
+ - aider_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 (aider)".
166
+ - --attribute-committer: Modify Committer name to "User Name (aider)".
167
+ - --attribute-co-authored-by: Add
168
+ "Co-authored-by: aider (<model>) <aider@aider.chat>" trailer to commit message.
169
+
170
+ Behavior Summary:
171
+
172
+ 1. When aider_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 aider_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 Aider 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(aider),
192
+ Committer=You(aider)
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(aider), Committer=You, Trailer added
197
+ - User commit (defaults): aider_edits=False -> Author=You, Committer=You(aider)
198
+ - User commit with explicit no-committer: aider_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 = aider_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 aider_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: aider ({model_name}) <aider@aider.chat>"
253
+
254
+ # Determine if author/committer names should be modified
255
+ # Author modification applies only to aider 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
+ aider_edits and effective_author and (not attribute_co_authored_by or author_explicit)
260
+ )
261
+
262
+ # Committer modification applies regardless of aider_edits (based on tests).
263
+ # It's used if effective_committer is True AND
264
+ # (it's not an aider edit with co-authored-by OR committer was explicitly set).
265
+ use_attribute_committer = effective_committer and (
266
+ not (aider_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 = "aider: " + 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} (aider)"
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
+ with Spinner(spinner_text):
345
+ if model.system_prompt_prefix:
346
+ current_system_content = model.system_prompt_prefix + "\n" + system_content
347
+ else:
348
+ current_system_content = system_content
349
+
350
+ messages = [
351
+ dict(role="system", content=current_system_content),
352
+ dict(role="user", content=content),
353
+ ]
354
+
355
+ num_tokens = model.token_count(messages)
356
+ max_tokens = model.info.get("max_input_tokens") or 0
357
+
358
+ if max_tokens and num_tokens > max_tokens:
359
+ continue
360
+
361
+ commit_message = await model.simple_send_with_retries(messages)
362
+ if commit_message:
363
+ break # Found a model that could generate the message
364
+
365
+ if not commit_message:
366
+ self.io.tool_error("Failed to generate commit message!")
367
+ return
368
+
369
+ commit_message = commit_message.strip()
370
+ if commit_message and commit_message[0] == '"' and commit_message[-1] == '"':
371
+ commit_message = commit_message[1:-1].strip()
372
+
373
+ return commit_message
374
+
375
+ def get_diffs(self, fnames=None):
376
+ # We always want diffs of index and working dir
377
+
378
+ current_branch_has_commits = False
379
+ try:
380
+ active_branch = self.repo.active_branch
381
+ try:
382
+ commits = self.repo.iter_commits(active_branch)
383
+ current_branch_has_commits = any(commits)
384
+ except ANY_GIT_ERROR:
385
+ pass
386
+ except (TypeError,) + ANY_GIT_ERROR:
387
+ pass
388
+
389
+ if not fnames:
390
+ fnames = []
391
+
392
+ diffs = ""
393
+ for fname in fnames:
394
+ if not self.path_in_repo(fname):
395
+ diffs += f"Added {fname}\n"
396
+
397
+ try:
398
+ if current_branch_has_commits:
399
+ args = ["HEAD", "--"] + list(fnames)
400
+ diffs += self.repo.git.diff(*args, stdout_as_string=False).decode(
401
+ self.io.encoding, "replace"
402
+ )
403
+ return diffs
404
+
405
+ wd_args = ["--"] + list(fnames)
406
+ index_args = ["--cached"] + wd_args
407
+
408
+ diffs += self.repo.git.diff(*index_args, stdout_as_string=False).decode(
409
+ self.io.encoding, "replace"
410
+ )
411
+ diffs += self.repo.git.diff(*wd_args, stdout_as_string=False).decode(
412
+ self.io.encoding, "replace"
413
+ )
414
+
415
+ return diffs
416
+ except ANY_GIT_ERROR as err:
417
+ self.io.tool_error(f"Unable to diff: {err}")
418
+
419
+ def diff_commits(self, pretty, from_commit, to_commit):
420
+ args = []
421
+ if pretty:
422
+ args += ["--color"]
423
+ else:
424
+ args += ["--color=never"]
425
+
426
+ args += [from_commit, to_commit]
427
+ diffs = self.repo.git.diff(*args, stdout_as_string=False).decode(
428
+ self.io.encoding, "replace"
429
+ )
430
+
431
+ return diffs
432
+
433
+ def get_tracked_files(self):
434
+ if not self.repo:
435
+ return []
436
+
437
+ try:
438
+ commit = self.repo.head.commit
439
+ except ValueError:
440
+ commit = None
441
+ except ANY_GIT_ERROR as err:
442
+ self.git_repo_error = err
443
+ self.io.tool_error(f"Unable to list files in git repo: {err}")
444
+ self.io.tool_output("Is your git repo corrupted?")
445
+ return []
446
+
447
+ files = set()
448
+ if commit:
449
+ if commit in self.tree_files:
450
+ files = self.tree_files[commit]
451
+ else:
452
+ try:
453
+ iterator = commit.tree.traverse()
454
+ blob = None # Initialize blob
455
+ while True:
456
+ try:
457
+ blob = next(iterator)
458
+ if blob.type == "blob": # blob is a file
459
+ files.add(blob.path)
460
+ except IndexError:
461
+ # Handle potential index error during tree traversal
462
+ # without relying on potentially unassigned 'blob'
463
+ self.io.tool_warning(
464
+ "GitRepo: Index error encountered while reading git tree object."
465
+ " Skipping."
466
+ )
467
+ continue
468
+ except StopIteration:
469
+ break
470
+ except ANY_GIT_ERROR as err:
471
+ self.git_repo_error = err
472
+ self.io.tool_error(f"Unable to list files in git repo: {err}")
473
+ self.io.tool_output("Is your git repo corrupted?")
474
+ return []
475
+ files = set(self.normalize_path(path) for path in files)
476
+ self.tree_files[commit] = set(files)
477
+
478
+ # Add staged files
479
+ index = self.repo.index
480
+ try:
481
+ staged_files = [path for path, _ in index.entries.keys()]
482
+ files.update(self.normalize_path(path) for path in staged_files)
483
+ except ANY_GIT_ERROR as err:
484
+ self.io.tool_error(f"Unable to read staged files: {err}")
485
+
486
+ res = [fname for fname in files if not self.ignored_file(fname)]
487
+
488
+ return res
489
+
490
+ def normalize_path(self, path):
491
+ orig_path = path
492
+ res = self.normalized_path.get(orig_path)
493
+ if res:
494
+ return res
495
+
496
+ path = str(Path(PurePosixPath((Path(self.root) / path).relative_to(self.root))))
497
+ self.normalized_path[orig_path] = path
498
+ return path
499
+
500
+ def refresh_aider_ignore(self):
501
+ if not self.aider_ignore_file:
502
+ return
503
+
504
+ current_time = time.time()
505
+ if current_time - self.aider_ignore_last_check < 1:
506
+ return
507
+
508
+ self.aider_ignore_last_check = current_time
509
+
510
+ if not self.aider_ignore_file.is_file():
511
+ return
512
+
513
+ mtime = self.aider_ignore_file.stat().st_mtime
514
+ if mtime != self.aider_ignore_ts:
515
+ self.aider_ignore_ts = mtime
516
+ self.ignore_file_cache = {}
517
+ lines = self.aider_ignore_file.read_text().splitlines()
518
+ self.aider_ignore_spec = pathspec.PathSpec.from_lines(
519
+ pathspec.patterns.GitWildMatchPattern,
520
+ lines,
521
+ )
522
+
523
+ def git_ignored_file(self, path):
524
+ if not self.repo:
525
+ return
526
+ try:
527
+ if self.repo.ignored(path):
528
+ return True
529
+ except ANY_GIT_ERROR:
530
+ return False
531
+
532
+ def ignored_file(self, fname):
533
+ self.refresh_aider_ignore()
534
+
535
+ if fname in self.ignore_file_cache:
536
+ return self.ignore_file_cache[fname]
537
+
538
+ result = self.ignored_file_raw(fname)
539
+ self.ignore_file_cache[fname] = result
540
+ return result
541
+
542
+ def ignored_file_raw(self, fname):
543
+ if self.subtree_only:
544
+ try:
545
+ fname_path = Path(self.normalize_path(fname))
546
+ cwd_path = Path.cwd().resolve().relative_to(Path(self.root).resolve())
547
+ except ValueError:
548
+ # Issue #1524
549
+ # ValueError: 'C:\\dev\\squid-certbot' is not in the subpath of
550
+ # 'C:\\dev\\squid-certbot'
551
+ # Clearly, fname is not under cwd... so ignore it
552
+ return True
553
+
554
+ if cwd_path not in fname_path.parents and fname_path != cwd_path:
555
+ return True
556
+
557
+ if not self.aider_ignore_file or not self.aider_ignore_file.is_file():
558
+ return False
559
+
560
+ try:
561
+ fname = self.normalize_path(fname)
562
+ except ValueError:
563
+ return True
564
+
565
+ return self.aider_ignore_spec.match_file(fname)
566
+
567
+ def path_in_repo(self, path):
568
+ if not self.repo:
569
+ return
570
+ if not path:
571
+ return
572
+
573
+ tracked_files = set(self.get_tracked_files())
574
+ return self.normalize_path(path) in tracked_files
575
+
576
+ def abs_root_path(self, path):
577
+ res = Path(self.root) / path
578
+ return utils.safe_abs_path(res)
579
+
580
+ def get_dirty_files(self):
581
+ """
582
+ Returns a list of all files which are dirty (not committed), either staged or in the working
583
+ directory.
584
+ """
585
+ dirty_files = set()
586
+
587
+ # Get staged files
588
+ staged_files = self.repo.git.diff("--name-only", "--cached").splitlines()
589
+ dirty_files.update(staged_files)
590
+
591
+ # Get unstaged files
592
+ unstaged_files = self.repo.git.diff("--name-only").splitlines()
593
+ dirty_files.update(unstaged_files)
594
+
595
+ return list(dirty_files)
596
+
597
+ def is_dirty(self, path=None):
598
+ if path and not self.path_in_repo(path):
599
+ return True
600
+
601
+ return self.repo.is_dirty(path=path)
602
+
603
+ def get_head_commit(self):
604
+ try:
605
+ return self.repo.head.commit
606
+ except (ValueError,) + ANY_GIT_ERROR:
607
+ return None
608
+
609
+ def get_head_commit_sha(self, short=False):
610
+ commit = self.get_head_commit()
611
+ if not commit:
612
+ return
613
+ if short:
614
+ return commit.hexsha[:7]
615
+ return commit.hexsha
616
+
617
+ def get_head_commit_message(self, default=None):
618
+ commit = self.get_head_commit()
619
+ if not commit:
620
+ return default
621
+ return commit.message