From e34e6ff897bad7ce352ea71eae85ecc3e9836470 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 11:14:15 -0300 Subject: [PATCH 01/40] wip --- aider/coders/base_coder.py | 211 ------------------------------------ aider/repo.py | 217 +++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 211 deletions(-) create mode 100644 aider/repo.py diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 14e076764..b9d48cd62 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -206,12 +206,6 @@ class Coder: self.root = utils.safe_abs_path(self.root) - def get_rel_repo_dir(self): - try: - return os.path.relpath(self.repo.git_dir, os.getcwd()) - except ValueError: - return self.repo.git_dir - def add_rel_fname(self, rel_fname): self.abs_fnames.add(self.abs_root_path(rel_fname)) @@ -219,72 +213,6 @@ class Coder: res = Path(self.root) / path return utils.safe_abs_path(res) - def set_repo(self, cmd_line_fnames): - if not cmd_line_fnames: - cmd_line_fnames = ["."] - - repo_paths = [] - for fname in cmd_line_fnames: - fname = Path(fname) - if not fname.exists(): - self.io.tool_output(f"Creating empty file {fname}") - fname.parent.mkdir(parents=True, exist_ok=True) - fname.touch() - - fname = fname.resolve() - - try: - repo_path = git.Repo(fname, search_parent_directories=True).working_dir - repo_path = utils.safe_abs_path(repo_path) - repo_paths.append(repo_path) - except git.exc.InvalidGitRepositoryError: - pass - - if fname.is_dir(): - continue - - self.abs_fnames.add(str(fname)) - - num_repos = len(set(repo_paths)) - - if num_repos == 0: - return - if num_repos > 1: - self.io.tool_error("Files are in different git repos.") - return - - # https://github.com/gitpython-developers/GitPython/issues/427 - self.repo = git.Repo(repo_paths.pop(), odbt=git.GitDB) - - self.root = utils.safe_abs_path(self.repo.working_tree_dir) - - new_files = [] - for fname in self.abs_fnames: - relative_fname = self.get_rel_fname(fname) - - tracked_files = set(self.get_tracked_files()) - if relative_fname not in tracked_files: - new_files.append(relative_fname) - - if new_files: - rel_repo_dir = self.get_rel_repo_dir() - - self.io.tool_output(f"Files not tracked in {rel_repo_dir}:") - for fn in new_files: - self.io.tool_output(f" - {fn}") - if self.io.confirm_ask("Add them?"): - for relative_fname in new_files: - self.repo.git.add(relative_fname) - self.io.tool_output(f"Added {relative_fname} to the git repo") - show_files = ", ".join(new_files) - commit_message = f"Added new files to the git repo: {show_files}" - self.repo.git.commit("-m", commit_message, "--no-verify") - commit_hash = self.repo.head.commit.hexsha[:7] - self.io.tool_output(f"Commit {commit_hash} {commit_message}") - else: - self.io.tool_error("Skipped adding new files to the git repo.") - return - # fences are obfuscated so aider can modify this file! fences = [ ("``" + "`", "``" + "`"), @@ -797,145 +725,6 @@ class Coder: def render_incremental_response(self, final): return self.partial_response_content - def get_context_from_history(self, history): - context = "" - if history: - for msg in history: - context += "\n" + msg["role"].upper() + ": " + msg["content"] + "\n" - return context - - def get_commit_message(self, diffs, context): - if len(diffs) >= 4 * 1024 * 4: - self.io.tool_error( - f"Diff is too large for {models.GPT35.name} to generate a commit message." - ) - return - - diffs = "# Diffs:\n" + diffs - - messages = [ - dict(role="system", content=prompts.commit_system), - dict(role="user", content=context + diffs), - ] - - try: - interrupted = self.send( - messages, - model=models.GPT35.name, - silent=True, - ) - except openai.error.InvalidRequestError: - self.io.tool_error( - f"Failed to generate commit message using {models.GPT35.name} due to an invalid" - " request." - ) - return - - commit_message = self.partial_response_content - commit_message = commit_message.strip() - if commit_message and commit_message[0] == '"' and commit_message[-1] == '"': - commit_message = commit_message[1:-1].strip() - - if interrupted: - self.io.tool_error( - f"Unable to get commit message from {models.GPT35.name}. Use /commit to try again." - ) - return - - return commit_message - - def get_diffs(self, *args): - if self.pretty: - args = ["--color"] + list(args) - - diffs = self.repo.git.diff(*args) - return diffs - - def commit(self, history=None, prefix=None, ask=False, message=None, which="chat_files"): - repo = self.repo - if not repo: - return - - if not repo.is_dirty(): - return - - def get_dirty_files_and_diffs(file_list): - diffs = "" - relative_dirty_files = [] - for fname in file_list: - relative_fname = self.get_rel_fname(fname) - relative_dirty_files.append(relative_fname) - - try: - current_branch_commit_count = len( - list(self.repo.iter_commits(self.repo.active_branch)) - ) - except git.exc.GitCommandError: - current_branch_commit_count = None - - if not current_branch_commit_count: - continue - - these_diffs = self.get_diffs("HEAD", "--", relative_fname) - - if these_diffs: - diffs += these_diffs + "\n" - - return relative_dirty_files, diffs - - if which == "repo_files": - all_files = [os.path.join(self.root, f) for f in self.get_all_relative_files()] - relative_dirty_fnames, diffs = get_dirty_files_and_diffs(all_files) - elif which == "chat_files": - relative_dirty_fnames, diffs = get_dirty_files_and_diffs(self.abs_fnames) - else: - raise ValueError(f"Invalid value for 'which': {which}") - - if self.show_diffs or ask: - # don't use io.tool_output() because we don't want to log or further colorize - print(diffs) - - context = self.get_context_from_history(history) - if message: - commit_message = message - else: - commit_message = self.get_commit_message(diffs, context) - - if not commit_message: - commit_message = "work in progress" - - if prefix: - commit_message = prefix + commit_message - - if ask: - if which == "repo_files": - self.io.tool_output("Git repo has uncommitted changes.") - else: - self.io.tool_output("Files have uncommitted changes.") - - res = self.io.prompt_ask( - "Commit before the chat proceeds [y/n/commit message]?", - default=commit_message, - ).strip() - self.last_asked_for_commit_time = self.get_last_modified() - - self.io.tool_output() - - if res.lower() in ["n", "no"]: - self.io.tool_error("Skipped commmit.") - return - if res.lower() not in ["y", "yes"] and res: - commit_message = res - - repo.git.add(*relative_dirty_fnames) - - full_commit_message = commit_message + "\n\n# Aider chat conversation:\n\n" + context - repo.git.commit("-m", full_commit_message, "--no-verify") - commit_hash = repo.head.commit.hexsha[:7] - self.io.tool_output(f"Commit {commit_hash} {commit_message}") - - return commit_hash, commit_message - def get_rel_fname(self, fname): return os.path.relpath(fname, self.root) diff --git a/aider/repo.py b/aider/repo.py new file mode 100644 index 000000000..ca05a721c --- /dev/null +++ b/aider/repo.py @@ -0,0 +1,217 @@ +import git + + +class AiderRepo: + repo = None + + def __init__(self, io, cmd_line_fnames): + self.io = io + + if not cmd_line_fnames: + cmd_line_fnames = ["."] + + repo_paths = [] + for fname in cmd_line_fnames: + fname = Path(fname) + if not fname.exists(): + self.io.tool_output(f"Creating empty file {fname}") + fname.parent.mkdir(parents=True, exist_ok=True) + fname.touch() + + fname = fname.resolve() + + try: + repo_path = git.Repo(fname, search_parent_directories=True).working_dir + repo_path = utils.safe_abs_path(repo_path) + repo_paths.append(repo_path) + except git.exc.InvalidGitRepositoryError: + pass + + if fname.is_dir(): + continue + + self.abs_fnames.add(str(fname)) + + num_repos = len(set(repo_paths)) + + if num_repos == 0: + return + if num_repos > 1: + self.io.tool_error("Files are in different git repos.") + return + + # https://github.com/gitpython-developers/GitPython/issues/427 + self.repo = git.Repo(repo_paths.pop(), odbt=git.GitDB) + + self.root = utils.safe_abs_path(self.repo.working_tree_dir) + + new_files = [] + for fname in self.abs_fnames: + relative_fname = self.get_rel_fname(fname) + + tracked_files = set(self.get_tracked_files()) + if relative_fname not in tracked_files: + new_files.append(relative_fname) + + if new_files: + rel_repo_dir = self.get_rel_repo_dir() + + self.io.tool_output(f"Files not tracked in {rel_repo_dir}:") + for fn in new_files: + self.io.tool_output(f" - {fn}") + if self.io.confirm_ask("Add them?"): + for relative_fname in new_files: + self.repo.git.add(relative_fname) + self.io.tool_output(f"Added {relative_fname} to the git repo") + show_files = ", ".join(new_files) + commit_message = f"Added new files to the git repo: {show_files}" + self.repo.git.commit("-m", commit_message, "--no-verify") + commit_hash = self.repo.head.commit.hexsha[:7] + self.io.tool_output(f"Commit {commit_hash} {commit_message}") + else: + self.io.tool_error("Skipped adding new files to the git repo.") + + def commit(self, history=None, prefix=None, ask=False, message=None, which="chat_files"): + repo = self.repo + if not repo: + return + + if not repo.is_dirty(): + return + + def get_dirty_files_and_diffs(file_list): + diffs = "" + relative_dirty_files = [] + for fname in file_list: + relative_fname = self.get_rel_fname(fname) + relative_dirty_files.append(relative_fname) + + try: + current_branch_commit_count = len( + list(self.repo.iter_commits(self.repo.active_branch)) + ) + except git.exc.GitCommandError: + current_branch_commit_count = None + + if not current_branch_commit_count: + continue + + these_diffs = self.get_diffs("HEAD", "--", relative_fname) + + if these_diffs: + diffs += these_diffs + "\n" + + return relative_dirty_files, diffs + + if which == "repo_files": + all_files = [os.path.join(self.root, f) for f in self.get_all_relative_files()] + relative_dirty_fnames, diffs = get_dirty_files_and_diffs(all_files) + elif which == "chat_files": + relative_dirty_fnames, diffs = get_dirty_files_and_diffs(self.abs_fnames) + else: + raise ValueError(f"Invalid value for 'which': {which}") + + if self.show_diffs or ask: + # don't use io.tool_output() because we don't want to log or further colorize + print(diffs) + + context = self.get_context_from_history(history) + if message: + commit_message = message + else: + commit_message = self.get_commit_message(diffs, context) + + if not commit_message: + commit_message = "work in progress" + + if prefix: + commit_message = prefix + commit_message + + if ask: + if which == "repo_files": + self.io.tool_output("Git repo has uncommitted changes.") + else: + self.io.tool_output("Files have uncommitted changes.") + + res = self.io.prompt_ask( + "Commit before the chat proceeds [y/n/commit message]?", + default=commit_message, + ).strip() + self.last_asked_for_commit_time = self.get_last_modified() + + self.io.tool_output() + + if res.lower() in ["n", "no"]: + self.io.tool_error("Skipped commmit.") + return + if res.lower() not in ["y", "yes"] and res: + commit_message = res + + repo.git.add(*relative_dirty_fnames) + + full_commit_message = commit_message + "\n\n# Aider chat conversation:\n\n" + context + repo.git.commit("-m", full_commit_message, "--no-verify") + commit_hash = repo.head.commit.hexsha[:7] + self.io.tool_output(f"Commit {commit_hash} {commit_message}") + + return commit_hash, commit_message + + def get_rel_repo_dir(self): + try: + return os.path.relpath(self.repo.git_dir, os.getcwd()) + except ValueError: + return self.repo.git_dir + + def get_context_from_history(self, history): + context = "" + if history: + for msg in history: + context += "\n" + msg["role"].upper() + ": " + msg["content"] + "\n" + return context + + def get_commit_message(self, diffs, context): + if len(diffs) >= 4 * 1024 * 4: + self.io.tool_error( + f"Diff is too large for {models.GPT35.name} to generate a commit message." + ) + return + + diffs = "# Diffs:\n" + diffs + + messages = [ + dict(role="system", content=prompts.commit_system), + dict(role="user", content=context + diffs), + ] + + try: + interrupted = self.send( + messages, + model=models.GPT35.name, + silent=True, + ) + except openai.error.InvalidRequestError: + self.io.tool_error( + f"Failed to generate commit message using {models.GPT35.name} due to an invalid" + " request." + ) + return + + commit_message = self.partial_response_content + commit_message = commit_message.strip() + if commit_message and commit_message[0] == '"' and commit_message[-1] == '"': + commit_message = commit_message[1:-1].strip() + + if interrupted: + self.io.tool_error( + f"Unable to get commit message from {models.GPT35.name}. Use /commit to try again." + ) + return + + return commit_message + + def get_diffs(self, *args): + if self.pretty: + args = ["--color"] + list(args) + + diffs = self.repo.git.diff(*args) + return diffs From 289887d94fae425d654f5d47e5a244d32c2a8161 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 11:21:41 -0300 Subject: [PATCH 02/40] refactor send_with_retries --- aider/coders/base_coder.py | 45 ++++---------------------------------- aider/sendchat.py | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 41 deletions(-) create mode 100644 aider/sendchat.py diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index b9d48cd62..89a9017bf 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -9,12 +9,9 @@ import traceback from json.decoder import JSONDecodeError from pathlib import Path, PurePosixPath -import backoff import git import openai -import requests from jsonschema import Draft7Validator -from openai.error import APIError, RateLimitError, ServiceUnavailableError, Timeout from rich.console import Console, Text from rich.live import Live from rich.markdown import Markdown @@ -22,6 +19,7 @@ from rich.markdown import Markdown from aider import models, prompts, utils from aider.commands import Commands from aider.repomap import RepoMap +from aider.sendchat import send_with_retries from ..dump import dump # noqa: F401 @@ -555,43 +553,6 @@ class Coder: return prompts.added_files.format(fnames=", ".join(mentioned_rel_fnames)) - @backoff.on_exception( - backoff.expo, - ( - Timeout, - APIError, - ServiceUnavailableError, - RateLimitError, - requests.exceptions.ConnectionError, - ), - max_tries=10, - on_backoff=lambda details: print( - f"{details.get('exception','Exception')}\nRetry in {details['wait']:.1f} seconds." - ), - ) - def send_with_retries(self, model, messages, functions): - kwargs = dict( - model=model, - messages=messages, - temperature=0, - stream=self.stream, - ) - if functions is not None: - kwargs["functions"] = self.functions - - # we are abusing the openai object to stash these values - if hasattr(openai, "api_deployment_id"): - kwargs["deployment_id"] = openai.api_deployment_id - if hasattr(openai, "api_engine"): - kwargs["engine"] = openai.api_engine - - # Generate SHA1 hash of kwargs and append it to chat_completion_call_hashes - hash_object = hashlib.sha1(json.dumps(kwargs, sort_keys=True).encode()) - self.chat_completion_call_hashes.append(hash_object.hexdigest()) - - res = openai.ChatCompletion.create(**kwargs) - return res - def send(self, messages, model=None, silent=False, functions=None): if not model: model = self.main_model.name @@ -601,7 +562,9 @@ class Coder: interrupted = False try: - completion = self.send_with_retries(model, messages, functions) + hash_object, completion = send_with_retries(model, messages, functions, self.stream) + self.chat_completion_call_hashes.append(hash_object.hexdigest()) + if self.stream: self.show_send_output_stream(completion, silent) else: diff --git a/aider/sendchat.py b/aider/sendchat.py new file mode 100644 index 000000000..9a4414230 --- /dev/null +++ b/aider/sendchat.py @@ -0,0 +1,44 @@ +import hashlib +import json + +import backoff +import openai +import requests +from openai.error import APIError, RateLimitError, ServiceUnavailableError, Timeout + + +@backoff.on_exception( + backoff.expo, + ( + Timeout, + APIError, + ServiceUnavailableError, + RateLimitError, + requests.exceptions.ConnectionError, + ), + max_tries=10, + on_backoff=lambda details: print( + f"{details.get('exception','Exception')}\nRetry in {details['wait']:.1f} seconds." + ), +) +def send_with_retries(model, messages, functions, stream): + kwargs = dict( + model=model, + messages=messages, + temperature=0, + stream=stream, + ) + if functions is not None: + kwargs["functions"] = functions + + # we are abusing the openai object to stash these values + if hasattr(openai, "api_deployment_id"): + kwargs["deployment_id"] = openai.api_deployment_id + if hasattr(openai, "api_engine"): + kwargs["engine"] = openai.api_engine + + # Generate SHA1 hash of kwargs and append it to chat_completion_call_hashes + hash_object = hashlib.sha1(json.dumps(kwargs, sort_keys=True).encode()) + + res = openai.ChatCompletion.create(**kwargs) + return hash_object, res From 23beb7cb5d82520782c52686807bc870f4ec4a4a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 11:49:19 -0300 Subject: [PATCH 03/40] wip --- aider/coders/base_coder.py | 75 ++++++++---------- aider/coders/single_wholefile_func_coder.py | 9 --- aider/coders/wholefile_coder.py | 9 --- aider/coders/wholefile_func_coder.py | 9 --- aider/commands.py | 6 +- aider/repo.py | 84 +++++++++++++-------- 6 files changed, 87 insertions(+), 105 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 89a9017bf..e7947961b 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -7,9 +7,8 @@ import sys import time import traceback from json.decoder import JSONDecodeError -from pathlib import Path, PurePosixPath +from pathlib import Path -import git import openai from jsonschema import Draft7Validator from rich.console import Console, Text @@ -18,6 +17,7 @@ from rich.markdown import Markdown from aider import models, prompts, utils from aider.commands import Commands +from aider.repo import AiderRepo from aider.repomap import RepoMap from aider.sendchat import send_with_retries @@ -149,12 +149,16 @@ class Coder: self.commands = Commands(self.io, self) if use_git: - self.set_repo(fnames) + try: + self.repo = AiderRepo(fnames) + self.root = self.repo.root + except FileNotFoundError: + self.repo = None else: self.abs_fnames = set([str(Path(fname).resolve()) for fname in fnames]) if self.repo: - rel_repo_dir = self.get_rel_repo_dir() + rel_repo_dir = self.repo.get_rel_repo_dir() self.io.tool_output(f"Git repo: {rel_repo_dir}") else: self.io.tool_output("Git repo: none") @@ -376,7 +380,7 @@ class Coder: if self.should_dirty_commit(inp): self.io.tool_output("Git repo has uncommitted changes, preparing commit...") - self.commit(ask=True, which="repo_files") + self.commit(ask=True, which="repo_files", pretty=self.pretty) # files changed, move cur messages back behind the files messages self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits) @@ -495,8 +499,16 @@ class Coder: ) ] + def get_context_from_history(self, history): + context = "" + if history: + for msg in history: + context += "\n" + msg["role"].upper() + ": " + msg["content"] + "\n" + return context + def auto_commit(self): - res = self.commit(history=self.cur_messages, prefix="aider: ") + context = self.get_context_from_history(self.cur_messages) + res = self.commit(context=context, prefix="aider: ", pretty=self.pretty) if res: commit_hash, commit_message = res self.last_aider_commit_hash = commit_hash @@ -553,7 +565,7 @@ class Coder: return prompts.added_files.format(fnames=", ".join(mentioned_rel_fnames)) - def send(self, messages, model=None, silent=False, functions=None): + def send(self, messages, model=None, functions=None): if not model: model = self.main_model.name @@ -566,25 +578,24 @@ class Coder: self.chat_completion_call_hashes.append(hash_object.hexdigest()) if self.stream: - self.show_send_output_stream(completion, silent) + self.show_send_output_stream(completion) else: - self.show_send_output(completion, silent) + self.show_send_output(completion) except KeyboardInterrupt: self.keyboard_interrupt() interrupted = True - if not silent: - if self.partial_response_content: - self.io.ai_output(self.partial_response_content) - elif self.partial_response_function_call: - # TODO: push this into subclasses - args = self.parse_partial_args() - if args: - self.io.ai_output(json.dumps(args, indent=4)) + if self.partial_response_content: + self.io.ai_output(self.partial_response_content) + elif self.partial_response_function_call: + # TODO: push this into subclasses + args = self.parse_partial_args() + if args: + self.io.ai_output(json.dumps(args, indent=4)) return interrupted - def show_send_output(self, completion, silent): + def show_send_output(self, completion): if self.verbose: print(completion) @@ -633,9 +644,9 @@ class Coder: self.io.console.print(show_resp) self.io.console.print(tokens) - def show_send_output_stream(self, completion, silent): + def show_send_output_stream(self, completion): live = None - if self.pretty and not silent: + if self.pretty: live = Live(vertical_overflow="scroll") try: @@ -664,9 +675,6 @@ class Coder: except AttributeError: pass - if silent: - continue - if self.pretty: self.live_incremental_response(live, False) else: @@ -697,7 +705,7 @@ class Coder: def get_all_relative_files(self): if self.repo: - files = self.get_tracked_files() + files = self.repo.get_tracked_files() else: files = self.get_inchat_relative_files() @@ -752,25 +760,6 @@ class Coder: return full_path - def get_tracked_files(self): - if not self.repo: - return [] - - try: - commit = self.repo.head.commit - except ValueError: - return set() - - files = [] - for blob in commit.tree.traverse(): - if blob.type == "blob": # blob is a file - files.append(blob.path) - - # convert to appropriate os.sep, since git always normalizes to / - res = set(str(Path(PurePosixPath(path))) for path in files) - - return res - apply_update_errors = 0 def apply_updates(self): diff --git a/aider/coders/single_wholefile_func_coder.py b/aider/coders/single_wholefile_func_coder.py index 2835c5f7c..2d99c3645 100644 --- a/aider/coders/single_wholefile_func_coder.py +++ b/aider/coders/single_wholefile_func_coder.py @@ -42,15 +42,6 @@ class SingleWholeFileFunctionCoder(Coder): else: self.cur_messages += [dict(role="assistant", content=self.partial_response_content)] - def get_context_from_history(self, history): - context = "" - if history: - context += "# Context:\n" - for msg in history: - if msg["role"] == "user": - context += msg["role"].upper() + ": " + msg["content"] + "\n" - return context - def render_incremental_response(self, final=False): if self.partial_response_content: return self.partial_response_content diff --git a/aider/coders/wholefile_coder.py b/aider/coders/wholefile_coder.py index 3ccd0dac5..efec434af 100644 --- a/aider/coders/wholefile_coder.py +++ b/aider/coders/wholefile_coder.py @@ -20,15 +20,6 @@ class WholeFileCoder(Coder): else: self.cur_messages += [dict(role="assistant", content=self.partial_response_content)] - def get_context_from_history(self, history): - context = "" - if history: - context += "# Context:\n" - for msg in history: - if msg["role"] == "user": - context += msg["role"].upper() + ": " + msg["content"] + "\n" - return context - def render_incremental_response(self, final): try: return self.update_files(mode="diff") diff --git a/aider/coders/wholefile_func_coder.py b/aider/coders/wholefile_func_coder.py index e51150482..ebf10764d 100644 --- a/aider/coders/wholefile_func_coder.py +++ b/aider/coders/wholefile_func_coder.py @@ -55,15 +55,6 @@ class WholeFileFunctionCoder(Coder): else: self.cur_messages += [dict(role="assistant", content=self.partial_response_content)] - def get_context_from_history(self, history): - context = "" - if history: - context += "# Context:\n" - for msg in history: - if msg["role"] == "user": - context += msg["role"].upper() + ": " + msg["content"] + "\n" - return context - def render_incremental_response(self, final=False): if self.partial_response_content: return self.partial_response_content diff --git a/aider/commands.py b/aider/commands.py index 10e9cd740..11af5ec7f 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -215,7 +215,9 @@ class Commands: return commits = f"{self.coder.last_aider_commit_hash}~1" - diff = self.coder.get_diffs(commits, self.coder.last_aider_commit_hash) + diff = self.coder.get_diffs( + commits, self.coder.last_aider_commit_hash, pretty=self.coder.pretty + ) # don't use io.tool_output() because we don't want to log or further colorize print(diff) @@ -247,7 +249,7 @@ class Commands: added_fnames = [] git_added = [] - git_files = self.coder.get_tracked_files() + git_files = self.coder.get_tracked_files() if self.coder.repo else [] all_matched_files = set() for word in args.split(): diff --git a/aider/repo.py b/aider/repo.py index ca05a721c..d570748bc 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -1,4 +1,11 @@ +import os +from pathlib import Path, PurePosixPath + import git +import openai + +from aider import models, prompts, utils +from aider.sendchat import send_with_retries class AiderRepo: @@ -30,23 +37,26 @@ class AiderRepo: if fname.is_dir(): continue - self.abs_fnames.add(str(fname)) - num_repos = len(set(repo_paths)) if num_repos == 0: - return + raise FileNotFoundError if num_repos > 1: self.io.tool_error("Files are in different git repos.") - return + raise FileNotFoundError # https://github.com/gitpython-developers/GitPython/issues/427 self.repo = git.Repo(repo_paths.pop(), odbt=git.GitDB) - self.root = utils.safe_abs_path(self.repo.working_tree_dir) + def ___(self, fnames): + + # TODO! + + self.abs_fnames.add(str(fname)) + new_files = [] - for fname in self.abs_fnames: + for fname in fnames: relative_fname = self.get_rel_fname(fname) tracked_files = set(self.get_tracked_files()) @@ -71,7 +81,12 @@ class AiderRepo: else: self.io.tool_error("Skipped adding new files to the git repo.") - def commit(self, history=None, prefix=None, ask=False, message=None, which="chat_files"): + def commit( + self, context=None, prefix=None, ask=False, message=None, which="chat_files", pretty=False + ): + + ## TODO! + repo = self.repo if not repo: return @@ -96,7 +111,7 @@ class AiderRepo: if not current_branch_commit_count: continue - these_diffs = self.get_diffs("HEAD", "--", relative_fname) + these_diffs = self.get_diffs(pretty, "HEAD", "--", relative_fname) if these_diffs: diffs += these_diffs + "\n" @@ -115,7 +130,6 @@ class AiderRepo: # don't use io.tool_output() because we don't want to log or further colorize print(diffs) - context = self.get_context_from_history(history) if message: commit_message = message else: @@ -162,13 +176,6 @@ class AiderRepo: except ValueError: return self.repo.git_dir - def get_context_from_history(self, history): - context = "" - if history: - for msg in history: - context += "\n" + msg["role"].upper() + ": " + msg["content"] + "\n" - return context - def get_commit_message(self, diffs, context): if len(diffs) >= 4 * 1024 * 4: self.io.tool_error( @@ -184,34 +191,45 @@ class AiderRepo: ] try: - interrupted = self.send( - messages, + _hash, response = send_with_retries( model=models.GPT35.name, - silent=True, - ) - except openai.error.InvalidRequestError: - self.io.tool_error( - f"Failed to generate commit message using {models.GPT35.name} due to an invalid" - " request." + messages=messages, + functions=None, + stream=False, ) + commit_message = completion.choices[0].message.content + except (AttributeError, openai.error.InvalidRequestError): + self.io.tool_error(f"Failed to generate commit message using {models.GPT35.name}") return - commit_message = self.partial_response_content commit_message = commit_message.strip() if commit_message and commit_message[0] == '"' and commit_message[-1] == '"': commit_message = commit_message[1:-1].strip() - if interrupted: - self.io.tool_error( - f"Unable to get commit message from {models.GPT35.name}. Use /commit to try again." - ) - return - return commit_message - def get_diffs(self, *args): - if self.pretty: + def get_diffs(self, pretty, *args): + if pretty: args = ["--color"] + list(args) diffs = self.repo.git.diff(*args) return diffs + + def get_tracked_files(self): + if not self.repo: + return [] + + try: + commit = self.repo.head.commit + except ValueError: + return set() + + files = [] + for blob in commit.tree.traverse(): + if blob.type == "blob": # blob is a file + files.append(blob.path) + + # convert to appropriate os.sep, since git always normalizes to / + res = set(str(Path(PurePosixPath(path))) for path in files) + + return res From 296e7614c4e990fb825e1315a4b1d23b4f7497e7 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 12:06:32 -0300 Subject: [PATCH 04/40] wip --- aider/coders/base_coder.py | 30 +++++++++--- aider/commands.py | 4 +- aider/repo.py | 95 +++++++++----------------------------- 3 files changed, 47 insertions(+), 82 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index e7947961b..ecf1cc870 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -379,12 +379,7 @@ class Coder: ) if self.should_dirty_commit(inp): - self.io.tool_output("Git repo has uncommitted changes, preparing commit...") - self.commit(ask=True, which="repo_files", pretty=self.pretty) - - # files changed, move cur messages back behind the files messages - self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits) - + self.dirty_commit() if inp.strip(): self.io.tool_output("Use up-arrow to retry previous command:", inp) return @@ -399,6 +394,27 @@ class Coder: return self.send_new_user_message(inp) + def dirty_commit(self): + self.io.tool_output("Git repo has uncommitted changes.") + self.repo.show_diffs(self.pretty) + self.last_asked_for_commit_time = self.get_last_modified() + res = self.io.prompt_ask( + "Commit before the chat proceeds [y/n/commit message]?", + default="y", + ).strip() + if res.lower() in ["n", "no"]: + self.io.tool_error("Skipped commmit.") + return + if res.lower() in ["y", "yes"]: + message = None + else: + message = res.strip() + + self.commit(message=message) + + # files changed, move cur messages back behind the files messages + self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits) + def fmt_system_reminder(self): prompt = self.gpt_prompts.system_reminder prompt = prompt.format(fence=self.fence) @@ -508,7 +524,7 @@ class Coder: def auto_commit(self): context = self.get_context_from_history(self.cur_messages) - res = self.commit(context=context, prefix="aider: ", pretty=self.pretty) + res = self.commit(context=context, prefix="aider: ") if res: commit_hash, commit_message = res self.last_aider_commit_hash = commit_hash diff --git a/aider/commands.py b/aider/commands.py index 11af5ec7f..5b6e96b50 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -216,7 +216,9 @@ class Commands: commits = f"{self.coder.last_aider_commit_hash}~1" diff = self.coder.get_diffs( - commits, self.coder.last_aider_commit_hash, pretty=self.coder.pretty + self.coder.pretty, + commits, + self.coder.last_aider_commit_hash, ) # don't use io.tool_output() because we don't want to log or further colorize diff --git a/aider/repo.py b/aider/repo.py index d570748bc..3167df4bd 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -50,7 +50,6 @@ class AiderRepo: self.root = utils.safe_abs_path(self.repo.working_tree_dir) def ___(self, fnames): - # TODO! self.abs_fnames.add(str(fname)) @@ -81,91 +80,27 @@ class AiderRepo: else: self.io.tool_error("Skipped adding new files to the git repo.") - def commit( - self, context=None, prefix=None, ask=False, message=None, which="chat_files", pretty=False - ): - - ## TODO! - - repo = self.repo - if not repo: + def commit(self, context=None, prefix=None, message=None): + if not self.repo.is_dirty(): return - if not repo.is_dirty(): - return - - def get_dirty_files_and_diffs(file_list): - diffs = "" - relative_dirty_files = [] - for fname in file_list: - relative_fname = self.get_rel_fname(fname) - relative_dirty_files.append(relative_fname) - - try: - current_branch_commit_count = len( - list(self.repo.iter_commits(self.repo.active_branch)) - ) - except git.exc.GitCommandError: - current_branch_commit_count = None - - if not current_branch_commit_count: - continue - - these_diffs = self.get_diffs(pretty, "HEAD", "--", relative_fname) - - if these_diffs: - diffs += these_diffs + "\n" - - return relative_dirty_files, diffs - - if which == "repo_files": - all_files = [os.path.join(self.root, f) for f in self.get_all_relative_files()] - relative_dirty_fnames, diffs = get_dirty_files_and_diffs(all_files) - elif which == "chat_files": - relative_dirty_fnames, diffs = get_dirty_files_and_diffs(self.abs_fnames) - else: - raise ValueError(f"Invalid value for 'which': {which}") - - if self.show_diffs or ask: - # don't use io.tool_output() because we don't want to log or further colorize - print(diffs) - if message: commit_message = message else: + diffs = self.get_diffs(False) commit_message = self.get_commit_message(diffs, context) if not commit_message: - commit_message = "work in progress" + commit_message = "(no commit message provided)" if prefix: commit_message = prefix + commit_message - if ask: - if which == "repo_files": - self.io.tool_output("Git repo has uncommitted changes.") - else: - self.io.tool_output("Files have uncommitted changes.") + if context: + commit_message = commit_message + "\n\n# Aider chat conversation:\n\n" + context - res = self.io.prompt_ask( - "Commit before the chat proceeds [y/n/commit message]?", - default=commit_message, - ).strip() - self.last_asked_for_commit_time = self.get_last_modified() - - self.io.tool_output() - - if res.lower() in ["n", "no"]: - self.io.tool_error("Skipped commmit.") - return - if res.lower() not in ["y", "yes"] and res: - commit_message = res - - repo.git.add(*relative_dirty_fnames) - - full_commit_message = commit_message + "\n\n# Aider chat conversation:\n\n" + context - repo.git.commit("-m", full_commit_message, "--no-verify") - commit_hash = repo.head.commit.hexsha[:7] + self.repo.git.commit("-a", "-m", commit_message, "--no-verify") + commit_hash = self.repo.head.commit.hexsha[:7] self.io.tool_output(f"Commit {commit_hash} {commit_message}") return commit_hash, commit_message @@ -197,7 +132,7 @@ class AiderRepo: functions=None, stream=False, ) - commit_message = completion.choices[0].message.content + commit_message = response.choices[0].message.content except (AttributeError, openai.error.InvalidRequestError): self.io.tool_error(f"Failed to generate commit message using {models.GPT35.name}") return @@ -215,6 +150,18 @@ class AiderRepo: diffs = self.repo.git.diff(*args) return diffs + def show_diffs(self, pretty): + try: + current_branch_has_commits = any(self.repo.iter_commits(self.repo.active_branch)) + except git.exc.GitCommandError: + current_branch_has_commits = False + + if not current_branch_has_commits: + return + + diffs = self.get_diffs(pretty, "HEAD") + print(diffs) + def get_tracked_files(self): if not self.repo: return [] From 14b24dc2fdb6ffb22e10e06f39ef112ae6167f67 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 12:57:01 -0300 Subject: [PATCH 05/40] works --- aider/coders/base_coder.py | 32 +++++++---- aider/main.py | 3 +- aider/repo.py | 109 ++++++++++++++++++------------------- 3 files changed, 74 insertions(+), 70 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index ecf1cc870..cac171a5e 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -148,14 +148,21 @@ class Coder: self.commands = Commands(self.io, self) + for fname in fnames: + fname = Path(fname) + if not fname.exists(): + self.io.tool_output(f"Creating empty file {fname}") + fname.parent.mkdir(parents=True, exist_ok=True) + fname.touch() + + self.abs_fnames.add(str(fname.resolve())) + if use_git: try: - self.repo = AiderRepo(fnames) + self.repo = AiderRepo(self.io, fnames) self.root = self.repo.root except FileNotFoundError: self.repo = None - else: - self.abs_fnames = set([str(Path(fname).resolve()) for fname in fnames]) if self.repo: rel_repo_dir = self.repo.get_rel_repo_dir() @@ -189,6 +196,8 @@ class Coder: for fname in self.get_inchat_relative_files(): self.io.tool_output(f"Added {fname} to the chat.") + self.repo.add_new_files(fnames) + # validate the functions jsonschema if self.functions: for function in self.functions: @@ -351,12 +360,6 @@ class Coder: if cmd in "add clear commit diff drop exit help ls tokens".split(): return - if not self.dirty_commits: - return - if not self.repo: - return - if not self.repo.is_dirty(): - return if self.last_asked_for_commit_time >= self.get_last_modified(): return return True @@ -395,6 +398,13 @@ class Coder: return self.send_new_user_message(inp) def dirty_commit(self): + if not self.dirty_commits: + return + if not self.repo: + return + if not self.repo.is_dirty(): + return + self.io.tool_output("Git repo has uncommitted changes.") self.repo.show_diffs(self.pretty) self.last_asked_for_commit_time = self.get_last_modified() @@ -410,7 +420,7 @@ class Coder: else: message = res.strip() - self.commit(message=message) + self.repo.commit(message=message) # files changed, move cur messages back behind the files messages self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits) @@ -524,7 +534,7 @@ class Coder: def auto_commit(self): context = self.get_context_from_history(self.cur_messages) - res = self.commit(context=context, prefix="aider: ") + res = self.repo.commit(context=context, prefix="aider: ") if res: commit_hash, commit_message = res self.last_aider_commit_hash = commit_hash diff --git a/aider/main.py b/aider/main.py index 7008bf943..3958f2d38 100644 --- a/aider/main.py +++ b/aider/main.py @@ -440,8 +440,7 @@ def main(args=None, input=None, output=None): io.tool_output(repo_map) return - if args.dirty_commits: - coder.commit(ask=True, which="repo_files") + coder.dirty_commit() if args.apply: content = io.read_text(args.apply) diff --git a/aider/repo.py b/aider/repo.py index 3167df4bd..531578048 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -7,24 +7,23 @@ import openai from aider import models, prompts, utils from aider.sendchat import send_with_retries +from .dump import dump # noqa: F401 + class AiderRepo: repo = None - def __init__(self, io, cmd_line_fnames): + def __init__(self, io, fnames): self.io = io - if not cmd_line_fnames: - cmd_line_fnames = ["."] + if fnames: + check_fnames = fnames + else: + check_fnames = ["."] repo_paths = [] - for fname in cmd_line_fnames: + for fname in check_fnames: fname = Path(fname) - if not fname.exists(): - self.io.tool_output(f"Creating empty file {fname}") - fname.parent.mkdir(parents=True, exist_ok=True) - fname.touch() - fname = fname.resolve() try: @@ -34,9 +33,6 @@ class AiderRepo: except git.exc.InvalidGitRepositoryError: pass - if fname.is_dir(): - continue - num_repos = len(set(repo_paths)) if num_repos == 0: @@ -49,36 +45,13 @@ class AiderRepo: self.repo = git.Repo(repo_paths.pop(), odbt=git.GitDB) self.root = utils.safe_abs_path(self.repo.working_tree_dir) - def ___(self, fnames): - # TODO! - - self.abs_fnames.add(str(fname)) - - new_files = [] + def add_new_files(self, fnames): + cur_files = [Path(fn).resolve() for fn in self.get_tracked_files()] for fname in fnames: - relative_fname = self.get_rel_fname(fname) - - tracked_files = set(self.get_tracked_files()) - if relative_fname not in tracked_files: - new_files.append(relative_fname) - - if new_files: - rel_repo_dir = self.get_rel_repo_dir() - - self.io.tool_output(f"Files not tracked in {rel_repo_dir}:") - for fn in new_files: - self.io.tool_output(f" - {fn}") - if self.io.confirm_ask("Add them?"): - for relative_fname in new_files: - self.repo.git.add(relative_fname) - self.io.tool_output(f"Added {relative_fname} to the git repo") - show_files = ", ".join(new_files) - commit_message = f"Added new files to the git repo: {show_files}" - self.repo.git.commit("-m", commit_message, "--no-verify") - commit_hash = self.repo.head.commit.hexsha[:7] - self.io.tool_output(f"Commit {commit_hash} {commit_message}") - else: - self.io.tool_error("Skipped adding new files to the git repo.") + if Path(fname).resolve() in cur_files: + continue + self.io.tool_output(f"Adding {fname} to git") + self.repo.git.add(fname) def commit(self, context=None, prefix=None, message=None): if not self.repo.is_dirty(): @@ -88,6 +61,7 @@ class AiderRepo: commit_message = message else: diffs = self.get_diffs(False) + dump(diffs) commit_message = self.get_commit_message(diffs, context) if not commit_message: @@ -96,10 +70,11 @@ class AiderRepo: if prefix: commit_message = prefix + commit_message + full_commit_message = commit_message if context: - commit_message = commit_message + "\n\n# Aider chat conversation:\n\n" + context + full_commit_message += "\n\n# Aider chat conversation:\n\n" + context - self.repo.git.commit("-a", "-m", commit_message, "--no-verify") + self.repo.git.commit("-a", "-m", full_commit_message, "--no-verify") commit_hash = self.repo.head.commit.hexsha[:7] self.io.tool_output(f"Commit {commit_hash} {commit_message}") @@ -120,21 +95,34 @@ class AiderRepo: diffs = "# Diffs:\n" + diffs + content = "" + if context: + content += context + "\n" + content += diffs + + dump(content) + messages = [ dict(role="system", content=prompts.commit_system), - dict(role="user", content=context + diffs), + dict(role="user", content=content), ] - try: - _hash, response = send_with_retries( - model=models.GPT35.name, - messages=messages, - functions=None, - stream=False, - ) - commit_message = response.choices[0].message.content - except (AttributeError, openai.error.InvalidRequestError): - self.io.tool_error(f"Failed to generate commit message using {models.GPT35.name}") + commit_message = None + for model in [models.GPT35.name, models.GPT35_16k.name]: + try: + _hash, response = send_with_retries( + model=models.GPT35.name, + messages=messages, + functions=None, + stream=False, + ) + commit_message = response.choices[0].message.content + break + except (AttributeError, openai.error.InvalidRequestError): + pass + + if not commit_message: + self.io.tool_error("Failed to generate commit message!") return commit_message = commit_message.strip() @@ -146,6 +134,8 @@ class AiderRepo: def get_diffs(self, pretty, *args): if pretty: args = ["--color"] + list(args) + if not args: + args = ["HEAD"] diffs = self.repo.git.diff(*args) return diffs @@ -156,10 +146,12 @@ class AiderRepo: except git.exc.GitCommandError: current_branch_has_commits = False - if not current_branch_has_commits: - return + dump(current_branch_has_commits) - diffs = self.get_diffs(pretty, "HEAD") + if not current_branch_has_commits: + return "" + + diffs = self.get_diffs(pretty) print(diffs) def get_tracked_files(self): @@ -180,3 +172,6 @@ class AiderRepo: res = set(str(Path(PurePosixPath(path))) for path in files) return res + + def is_dirty(self): + return self.repo.is_dirty() From 74875eac6ecfa11021f1610612b84ded17476497 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 12:58:23 -0300 Subject: [PATCH 06/40] sssh --- aider/coders/base_coder.py | 4 ++-- aider/repo.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index cac171a5e..34ffdf92a 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -381,8 +381,7 @@ class Coder: self.commands, ) - if self.should_dirty_commit(inp): - self.dirty_commit() + if self.should_dirty_commit(inp) and self.dirty_commit(): if inp.strip(): self.io.tool_output("Use up-arrow to retry previous command:", inp) return @@ -424,6 +423,7 @@ class Coder: # files changed, move cur messages back behind the files messages self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits) + return True def fmt_system_reminder(self): prompt = self.gpt_prompts.system_reminder diff --git a/aider/repo.py b/aider/repo.py index 531578048..330827adf 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -61,7 +61,6 @@ class AiderRepo: commit_message = message else: diffs = self.get_diffs(False) - dump(diffs) commit_message = self.get_commit_message(diffs, context) if not commit_message: @@ -100,8 +99,6 @@ class AiderRepo: content += context + "\n" content += diffs - dump(content) - messages = [ dict(role="system", content=prompts.commit_system), dict(role="user", content=content), @@ -146,8 +143,6 @@ class AiderRepo: except git.exc.GitCommandError: current_branch_has_commits = False - dump(current_branch_has_commits) - if not current_branch_has_commits: return "" From 01ea6919574f5e349b2ac14a1615a653f5a90990 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 13:01:48 -0300 Subject: [PATCH 07/40] Update commands to use the underlying repo --- aider/commands.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index 5b6e96b50..960d94e46 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -171,10 +171,10 @@ class Commands: ) return - local_head = self.coder.repo.git.rev_parse("HEAD") - current_branch = self.coder.repo.active_branch.name + local_head = self.coder.repo.repo.git.rev_parse("HEAD") + current_branch = self.coder.repo.repo.active_branch.name try: - remote_head = self.coder.repo.git.rev_parse(f"origin/{current_branch}") + remote_head = self.coder.repo.repo.git.rev_parse(f"origin/{current_branch}") has_origin = True except git.exc.GitCommandError: has_origin = False @@ -187,14 +187,14 @@ class Commands: ) return - last_commit = self.coder.repo.head.commit + last_commit = self.coder.repo.repo.head.commit if ( not last_commit.message.startswith("aider:") or last_commit.hexsha[:7] != self.coder.last_aider_commit_hash ): self.io.tool_error("The last commit was not made by aider in this chat session.") return - self.coder.repo.git.reset("--hard", "HEAD~1") + self.coder.repo.repo.git.reset("--hard", "HEAD~1") self.io.tool_output( f"{last_commit.message.strip()}\n" f"The above commit {self.coder.last_aider_commit_hash} " @@ -215,7 +215,7 @@ class Commands: return commits = f"{self.coder.last_aider_commit_hash}~1" - diff = self.coder.get_diffs( + diff = self.coder.repo.get_diffs( self.coder.pretty, commits, self.coder.last_aider_commit_hash, @@ -278,7 +278,7 @@ class Commands: abs_file_path = self.coder.abs_root_path(matched_file) if self.coder.repo and matched_file not in git_files: - self.coder.repo.git.add(abs_file_path) + self.coder.repo.repo.git.add(abs_file_path) git_added.append(matched_file) if abs_file_path in self.coder.abs_fnames: @@ -295,8 +295,8 @@ class Commands: if self.coder.repo and git_added: git_added = " ".join(git_added) commit_message = f"aider: Added {git_added}" - self.coder.repo.git.commit("-m", commit_message, "--no-verify") - commit_hash = self.coder.repo.head.commit.hexsha[:7] + self.coder.repo.repo.git.commit("-m", commit_message, "--no-verify") + commit_hash = self.coder.repo.repo.head.commit.hexsha[:7] self.io.tool_output(f"Commit {commit_hash} {commit_message}") if not added_fnames: From 317412f854019b974a1f19d7621e7df85ad9d54f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 13:56:48 -0300 Subject: [PATCH 08/40] noop --- aider/coders/base_coder.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 34ffdf92a..3d52c4b80 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -351,19 +351,6 @@ class Coder: self.last_keyboard_interrupt = now - def should_dirty_commit(self, inp): - cmds = self.commands.matching_commands(inp) - if cmds: - matching_commands, _, _ = cmds - if len(matching_commands) == 1: - cmd = matching_commands[0][1:] - if cmd in "add clear commit diff drop exit help ls tokens".split(): - return - - if self.last_asked_for_commit_time >= self.get_last_modified(): - return - return True - def move_back_cur_messages(self, message): self.done_messages += self.cur_messages if message: @@ -396,6 +383,19 @@ class Coder: return self.send_new_user_message(inp) + def should_dirty_commit(self, inp): + cmds = self.commands.matching_commands(inp) + if cmds: + matching_commands, _, _ = cmds + if len(matching_commands) == 1: + cmd = matching_commands[0][1:] + if cmd in "add clear commit diff drop exit help ls tokens".split(): + return + + if self.last_asked_for_commit_time >= self.get_last_modified(): + return + return True + def dirty_commit(self): if not self.dirty_commits: return From 5a163237b4323b461bd60f865261142002e51bca Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 13:59:49 -0300 Subject: [PATCH 09/40] noop --- aider/coders/base_coder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 3d52c4b80..f36a4bf18 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -224,7 +224,6 @@ class Coder: res = Path(self.root) / path return utils.safe_abs_path(res) - # fences are obfuscated so aider can modify this file! fences = [ ("``" + "`", "``" + "`"), wrap_fence("source"), From 4f94f720b5d2a6cd64717fb21e465d1451352545 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:05:30 -0300 Subject: [PATCH 10/40] cleanup --- aider/coders/base_coder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index f36a4bf18..9d39d5ccd 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -543,8 +543,7 @@ class Coder: message=commit_message, ) else: - if self.repo: - self.io.tool_output("No changes made to git tracked files.") + self.io.tool_output("No changes made to git tracked files.") saved_message = self.gpt_prompts.files_content_gpt_no_edits return saved_message From 0ac6957d04cb97b7e10ad33486442916a9770df8 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:07:30 -0300 Subject: [PATCH 11/40] noop --- aider/coders/base_coder.py | 134 +++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 66 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 9d39d5ccd..92bde8379 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -382,48 +382,6 @@ class Coder: return self.send_new_user_message(inp) - def should_dirty_commit(self, inp): - cmds = self.commands.matching_commands(inp) - if cmds: - matching_commands, _, _ = cmds - if len(matching_commands) == 1: - cmd = matching_commands[0][1:] - if cmd in "add clear commit diff drop exit help ls tokens".split(): - return - - if self.last_asked_for_commit_time >= self.get_last_modified(): - return - return True - - def dirty_commit(self): - if not self.dirty_commits: - return - if not self.repo: - return - if not self.repo.is_dirty(): - return - - self.io.tool_output("Git repo has uncommitted changes.") - self.repo.show_diffs(self.pretty) - self.last_asked_for_commit_time = self.get_last_modified() - res = self.io.prompt_ask( - "Commit before the chat proceeds [y/n/commit message]?", - default="y", - ).strip() - if res.lower() in ["n", "no"]: - self.io.tool_error("Skipped commmit.") - return - if res.lower() in ["y", "yes"]: - message = None - else: - message = res.strip() - - self.repo.commit(message=message) - - # files changed, move cur messages back behind the files messages - self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits) - return True - def fmt_system_reminder(self): prompt = self.gpt_prompts.system_reminder prompt = prompt.format(fence=self.fence) @@ -524,30 +482,6 @@ class Coder: ) ] - def get_context_from_history(self, history): - context = "" - if history: - for msg in history: - context += "\n" + msg["role"].upper() + ": " + msg["content"] + "\n" - return context - - def auto_commit(self): - context = self.get_context_from_history(self.cur_messages) - res = self.repo.commit(context=context, prefix="aider: ") - if res: - commit_hash, commit_message = res - self.last_aider_commit_hash = commit_hash - - saved_message = self.gpt_prompts.files_content_gpt_edits.format( - hash=commit_hash, - message=commit_message, - ) - else: - self.io.tool_output("No changes made to git tracked files.") - saved_message = self.gpt_prompts.files_content_gpt_no_edits - - return saved_message - def check_for_file_mentions(self, content): words = set(word for word in content.split()) @@ -852,6 +786,74 @@ class Coder: except JSONDecodeError: pass + # commits... + + def get_context_from_history(self, history): + context = "" + if history: + for msg in history: + context += "\n" + msg["role"].upper() + ": " + msg["content"] + "\n" + return context + + def auto_commit(self): + context = self.get_context_from_history(self.cur_messages) + res = self.repo.commit(context=context, prefix="aider: ") + if res: + commit_hash, commit_message = res + self.last_aider_commit_hash = commit_hash + + saved_message = self.gpt_prompts.files_content_gpt_edits.format( + hash=commit_hash, + message=commit_message, + ) + else: + self.io.tool_output("No changes made to git tracked files.") + saved_message = self.gpt_prompts.files_content_gpt_no_edits + + return saved_message + + def should_dirty_commit(self, inp): + cmds = self.commands.matching_commands(inp) + if cmds: + matching_commands, _, _ = cmds + if len(matching_commands) == 1: + cmd = matching_commands[0][1:] + if cmd in "add clear commit diff drop exit help ls tokens".split(): + return + + if self.last_asked_for_commit_time >= self.get_last_modified(): + return + return True + + def dirty_commit(self): + if not self.dirty_commits: + return + if not self.repo: + return + if not self.repo.is_dirty(): + return + + self.io.tool_output("Git repo has uncommitted changes.") + self.repo.show_diffs(self.pretty) + self.last_asked_for_commit_time = self.get_last_modified() + res = self.io.prompt_ask( + "Commit before the chat proceeds [y/n/commit message]?", + default="y", + ).strip() + if res.lower() in ["n", "no"]: + self.io.tool_error("Skipped commmit.") + return + if res.lower() in ["y", "yes"]: + message = None + else: + message = res.strip() + + self.repo.commit(message=message) + + # files changed, move cur messages back behind the files messages + self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits) + return True + def check_model_availability(main_model): available_models = openai.Model.list() From d0be8d4cbd345cbb901608444a079d672b0e4eb1 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:09:38 -0300 Subject: [PATCH 12/40] cleanup --- aider/coders/base_coder.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 92bde8379..3bb4da93b 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -798,19 +798,17 @@ class Coder: def auto_commit(self): context = self.get_context_from_history(self.cur_messages) res = self.repo.commit(context=context, prefix="aider: ") - if res: + if not res: commit_hash, commit_message = res self.last_aider_commit_hash = commit_hash - saved_message = self.gpt_prompts.files_content_gpt_edits.format( + return self.gpt_prompts.files_content_gpt_edits.format( hash=commit_hash, message=commit_message, ) - else: - self.io.tool_output("No changes made to git tracked files.") - saved_message = self.gpt_prompts.files_content_gpt_no_edits - return saved_message + self.io.tool_output("No changes made to git tracked files.") + return self.gpt_prompts.files_content_gpt_no_edits def should_dirty_commit(self, inp): cmds = self.commands.matching_commands(inp) From 28578217daac9302ea0dac6a9ac58009f5ee8044 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:17:14 -0300 Subject: [PATCH 13/40] move tests to test_sendchat.py --- aider/coders/base_coder.py | 3 ++- tests/test_coder.py | 45 -------------------------------------- tests/test_sendchat.py | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 46 deletions(-) create mode 100644 tests/test_sendchat.py diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 3bb4da93b..0a39c8ace 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -196,7 +196,8 @@ class Coder: for fname in self.get_inchat_relative_files(): self.io.tool_output(f"Added {fname} to the chat.") - self.repo.add_new_files(fnames) + if self.repo: + self.repo.add_new_files(fnames) # validate the functions jsonschema if self.functions: diff --git a/tests/test_coder.py b/tests/test_coder.py index 41ae7e644..0258f5db6 100644 --- a/tests/test_coder.py +++ b/tests/test_coder.py @@ -6,7 +6,6 @@ from unittest.mock import MagicMock, patch import git import openai -import requests from aider import models from aider.coders import Coder @@ -215,50 +214,6 @@ class TestCoder(unittest.TestCase): # Assert that the returned message is the expected one self.assertEqual(result, 'a good "commit message"') - @patch("aider.coders.base_coder.openai.ChatCompletion.create") - @patch("builtins.print") - def test_send_with_retries_rate_limit_error(self, mock_print, mock_chat_completion_create): - # Mock the IO object - mock_io = MagicMock() - - # Initialize the Coder object with the mocked IO and mocked repo - coder = Coder.create(models.GPT4, None, mock_io) - - # Set up the mock to raise RateLimitError on - # the first call and return None on the second call - mock_chat_completion_create.side_effect = [ - openai.error.RateLimitError("Rate limit exceeded"), - None, - ] - - # Call the send_with_retries method - coder.send_with_retries("model", ["message"], None) - - # Assert that print was called once - mock_print.assert_called_once() - - @patch("aider.coders.base_coder.openai.ChatCompletion.create") - @patch("builtins.print") - def test_send_with_retries_connection_error(self, mock_print, mock_chat_completion_create): - # Mock the IO object - mock_io = MagicMock() - - # Initialize the Coder object with the mocked IO and mocked repo - coder = Coder.create(models.GPT4, None, mock_io) - - # Set up the mock to raise ConnectionError on the first call - # and return None on the second call - mock_chat_completion_create.side_effect = [ - requests.exceptions.ConnectionError("Connection error"), - None, - ] - - # Call the send_with_retries method - coder.send_with_retries("model", ["message"], None) - - # Assert that print was called once - mock_print.assert_called_once() - def test_run_with_file_deletion(self): # Create a few temporary files diff --git a/tests/test_sendchat.py b/tests/test_sendchat.py new file mode 100644 index 000000000..59c6f8c80 --- /dev/null +++ b/tests/test_sendchat.py @@ -0,0 +1,41 @@ +import unittest +from unittest.mock import patch + +import openai +import requests + +from aider.sendchat import send_with_retries + + +class TestSendChat(unittest.TestCase): + @patch("aider.sendchat.openai.ChatCompletion.create") + @patch("builtins.print") + def test_send_with_retries_rate_limit_error(self, mock_print, mock_chat_completion_create): + # Set up the mock to raise RateLimitError on + # the first call and return None on the second call + mock_chat_completion_create.side_effect = [ + openai.error.RateLimitError("Rate limit exceeded"), + None, + ] + + # Call the send_with_retries method + send_with_retries("model", ["message"], None, False) + + # Assert that print was called once + mock_print.assert_called_once() + + @patch("aider.sendchat.openai.ChatCompletion.create") + @patch("builtins.print") + def test_send_with_retries_connection_error(self, mock_print, mock_chat_completion_create): + # Set up the mock to raise ConnectionError on the first call + # and return None on the second call + mock_chat_completion_create.side_effect = [ + requests.exceptions.ConnectionError("Connection error"), + None, + ] + + # Call the send_with_retries method + send_with_retries("model", ["message"], None, False) + + # Assert that print was called once + mock_print.assert_called_once() From ebd8d9725b7a1bb4df52eaefaca32e42736fe94d Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:24:19 -0300 Subject: [PATCH 14/40] wip --- tests/test_coder.py | 84 ----------------------------------------- tests/test_repo.py | 91 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 84 deletions(-) create mode 100644 tests/test_repo.py diff --git a/tests/test_coder.py b/tests/test_coder.py index 0258f5db6..e15870b84 100644 --- a/tests/test_coder.py +++ b/tests/test_coder.py @@ -154,46 +154,6 @@ class TestCoder(unittest.TestCase): self.assertEqual(coder.abs_fnames, set([str(fname.resolve())])) - def test_get_commit_message(self): - # Mock the IO object - mock_io = MagicMock() - - # Initialize the Coder object with the mocked IO and mocked repo - coder = Coder.create(models.GPT4, None, mock_io) - - # Mock the send method to set partial_response_content and return False - def mock_send(*args, **kwargs): - coder.partial_response_content = "a good commit message" - return False - - coder.send = MagicMock(side_effect=mock_send) - - # Call the get_commit_message method with dummy diff and context - result = coder.get_commit_message("dummy diff", "dummy context") - - # Assert that the returned message is the expected one - self.assertEqual(result, "a good commit message") - - def test_get_commit_message_strip_quotes(self): - # Mock the IO object - mock_io = MagicMock() - - # Initialize the Coder object with the mocked IO and mocked repo - coder = Coder.create(models.GPT4, None, mock_io) - - # Mock the send method to set partial_response_content and return False - def mock_send(*args, **kwargs): - coder.partial_response_content = "a good commit message" - return False - - coder.send = MagicMock(side_effect=mock_send) - - # Call the get_commit_message method with dummy diff and context - result = coder.get_commit_message("dummy diff", "dummy context") - - # Assert that the returned message is the expected one - self.assertEqual(result, "a good commit message") - def test_get_commit_message_no_strip_unmatched_quotes(self): # Mock the IO object mock_io = MagicMock() @@ -351,49 +311,5 @@ class TestCoder(unittest.TestCase): with self.assertRaises(openai.error.InvalidRequestError): coder.run(with_message="hi") - def test_get_tracked_files(self): - # Create a temporary directory - tempdir = Path(tempfile.mkdtemp()) - - # Initialize a git repository in the temporary directory and set user name and email - repo = git.Repo.init(tempdir) - repo.config_writer().set_value("user", "name", "Test User").release() - repo.config_writer().set_value("user", "email", "testuser@example.com").release() - - # Create three empty files and add them to the git repository - filenames = ["README.md", "subdir/fänny.md", "systemüber/blick.md", 'file"with"quotes.txt'] - created_files = [] - for filename in filenames: - file_path = tempdir / filename - try: - file_path.parent.mkdir(parents=True, exist_ok=True) - file_path.touch() - repo.git.add(str(file_path)) - created_files.append(Path(filename)) - except OSError: - # windows won't allow files with quotes, that's ok - self.assertIn('"', filename) - self.assertEqual(os.name, "nt") - - self.assertTrue(len(created_files) >= 3) - - repo.git.commit("-m", "added") - - # Create a Coder object on the temporary directory - coder = Coder.create( - models.GPT4, - None, - io=InputOutput(), - fnames=[str(tempdir / filenames[0])], - ) - - tracked_files = coder.get_tracked_files() - - # On windows, paths will come back \like\this, so normalize them back to Paths - tracked_files = [Path(fn) for fn in tracked_files] - - # Assert that coder.get_tracked_files() returns the three filenames - self.assertEqual(set(tracked_files), set(created_files)) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_repo.py b/tests/test_repo.py new file mode 100644 index 000000000..b54ceccbe --- /dev/null +++ b/tests/test_repo.py @@ -0,0 +1,91 @@ +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +import git +import openai + +from aider import models +from aider.dump import dump # noqa: F401 +from aider.io import InputOutput +from aider.repo import AiderRepo +from tests.utils import GitTemporaryDirectory + + +class TestRepo(unittest.TestCase): + @patch("aider.sendchat.send_with_retries") + def test_get_commit_message(self, mock_send): + # TODO: make aider.sendchat.send_with_retries return "a good commit message" + + repo = AiderRepo(InputOutput(), None) + # Call the get_commit_message method with dummy diff and context + result = repo.get_commit_message("dummy diff", "dummy context") + + # Assert that the returned message is the expected one + self.assertEqual(result, "a good commit message") + + def test_get_commit_message_strip_quotes(self): + # Mock the IO object + mock_io = MagicMock() + + # Initialize the Coder object with the mocked IO and mocked repo + coder = Coder.create(models.GPT4, None, mock_io) + + # Mock the send method to set partial_response_content and return False + def mock_send(*args, **kwargs): + coder.partial_response_content = "a good commit message" + return False + + coder.send = MagicMock(side_effect=mock_send) + + # Call the get_commit_message method with dummy diff and context + result = coder.get_commit_message("dummy diff", "dummy context") + + # Assert that the returned message is the expected one + self.assertEqual(result, "a good commit message") + + def test_get_tracked_files(self): + # Create a temporary directory + tempdir = Path(tempfile.mkdtemp()) + + # Initialize a git repository in the temporary directory and set user name and email + repo = git.Repo.init(tempdir) + repo.config_writer().set_value("user", "name", "Test User").release() + repo.config_writer().set_value("user", "email", "testuser@example.com").release() + + # Create three empty files and add them to the git repository + filenames = ["README.md", "subdir/fänny.md", "systemüber/blick.md", 'file"with"quotes.txt'] + created_files = [] + for filename in filenames: + file_path = tempdir / filename + try: + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.touch() + repo.git.add(str(file_path)) + created_files.append(Path(filename)) + except OSError: + # windows won't allow files with quotes, that's ok + self.assertIn('"', filename) + self.assertEqual(os.name, "nt") + + self.assertTrue(len(created_files) >= 3) + + repo.git.commit("-m", "added") + + # Create a Coder object on the temporary directory + coder = Coder.create( + models.GPT4, + None, + io=InputOutput(), + fnames=[str(tempdir / filenames[0])], + ) + + tracked_files = coder.get_tracked_files() + + # On windows, paths will come back \like\this, so normalize them back to Paths + tracked_files = [Path(fn) for fn in tracked_files] + + # Assert that coder.get_tracked_files() returns the three filenames + self.assertEqual(set(tracked_files), set(created_files)) From 6f3b498228925988909befe21400b01fe8b9594f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:24:49 -0300 Subject: [PATCH 15/40] aider: Set the return value of the mocked function `aider.sendchat.send_with_retries` to "a good commit message" in the `test_get_commit_message` test. # Aider chat conversation: USER: do the todo ASSISTANT: To complete the TODO, we need to mock the return value of the function `aider.sendchat.send_with_retries`. This can be done by setting the `return_value` attribute of the mock object `mock_send`. Here is the change: ```python tests/test_repo.py <<<<<<< ORIGINAL @patch("aider.sendchat.send_with_retries") def test_get_commit_message(self, mock_send): # TODO: make aider.sendchat.send_with_retries return "a good commit message" repo = AiderRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context result = repo.get_commit_message("dummy diff", "dummy context") # Assert that the returned message is the expected one self.assertEqual(result, "a good commit_message") ======= @patch("aider.sendchat.send_with_retries") def test_get_commit_message(self, mock_send): # Set the return value of the mocked function mock_send.return_value = "a good commit message" repo = AiderRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context result = repo.get_commit_message("dummy diff", "dummy context") # Assert that the returned message is the expected one self.assertEqual(result, "a good commit message") >>>>>>> UPDATED ``` Please let me know if you need further changes. --- tests/test_repo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_repo.py b/tests/test_repo.py index b54ceccbe..41a8a791d 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -17,7 +17,8 @@ from tests.utils import GitTemporaryDirectory class TestRepo(unittest.TestCase): @patch("aider.sendchat.send_with_retries") def test_get_commit_message(self, mock_send): - # TODO: make aider.sendchat.send_with_retries return "a good commit message" + # Set the return value of the mocked function + mock_send.return_value = "a good commit message" repo = AiderRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context From a789de9433241b2053af2374f9cade87d94eb4ee Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:27:31 -0300 Subject: [PATCH 16/40] Refactor test_get_commit_message to use the correct import and handle the return value of mock_send correctly. --- tests/test_repo.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_repo.py b/tests/test_repo.py index 41a8a791d..efa18bf99 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -15,10 +15,13 @@ from tests.utils import GitTemporaryDirectory class TestRepo(unittest.TestCase): - @patch("aider.sendchat.send_with_retries") + @patch("aider.repo.send_with_retries") def test_get_commit_message(self, mock_send): # Set the return value of the mocked function - mock_send.return_value = "a good commit message" + mock_send.return_value = ( + None, + "a good commit message" + ) repo = AiderRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context From 4a8add1b5d1ec8bc6e63fa200e0a524261105bc1 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:28:06 -0300 Subject: [PATCH 17/40] Refactor `get_tracked_files` method to be called on `self.coder.repo` instead of `self.coder`. --- aider/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index 960d94e46..22806fb7b 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -240,7 +240,7 @@ class Commands: # if repo, filter against it if self.coder.repo: - git_files = self.coder.get_tracked_files() + git_files = self.coder.repo.get_tracked_files() matched_files = [fn for fn in matched_files if str(fn) in git_files] res = list(map(str, matched_files)) @@ -251,7 +251,7 @@ class Commands: added_fnames = [] git_added = [] - git_files = self.coder.get_tracked_files() if self.coder.repo else [] + git_files = self.coder.repo.get_tracked_files() if self.coder.repo else [] all_matched_files = set() for word in args.split(): From 1733fe4c90ef615eb9fe4bb38de5bebd822907ef Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:29:15 -0300 Subject: [PATCH 18/40] aider: To match the return value of `mock_send` with `commit_message = response.choices[0].message.content`, modify the return value of `mock_send` in the `test_get_commit_message` function in the `tests/test_repo.py` file. # Aider chat conversation: USER: in `test_get_commit_message`, the mock_send return value needs to match the commit_message = response.choices[0].message.content ASSISTANT: To match the return value of `mock_send` with `commit_message = response.choices[0].message.content`, we need to modify the return value of `mock_send` in the `test_get_commit_message` function in the `tests/test_repo.py` file. The `send_with_retries` function returns a tuple where the second element is the response from the OpenAI API. The response is expected to be an object with a `choices` attribute, which is a list of objects, each with a `message` attribute, which in turn has a `content` attribute. So, we need to return a mock response object that has these attributes. Here's how to do it: ```python tests/test_repo.py <<<<<<< ORIGINAL mock_send.return_value = ( None, "a good commit message" ) ======= mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message.content = "a good commit message" mock_send.return_value = ( None, mock_response ) >>>>>>> UPDATED ``` This change will make the `mock_send` function return a mock response object that has the same structure as the actual response from the OpenAI API, allowing the `get_commit_message` method to correctly extract the commit message. --- tests/test_repo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_repo.py b/tests/test_repo.py index efa18bf99..0bcda17ee 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -18,9 +18,12 @@ class TestRepo(unittest.TestCase): @patch("aider.repo.send_with_retries") def test_get_commit_message(self, mock_send): # Set the return value of the mocked function + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "a good commit message" mock_send.return_value = ( None, - "a good commit message" + mock_response ) repo = AiderRepo(InputOutput(), None) From b9dc419f1974b6ac4b02e5ee88e2a36ac1bdf09b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:30:33 -0300 Subject: [PATCH 19/40] Refactor test_get_commit_message_strip_quotes to use a mocked send_with_retries function and set the return value of the mocked function. --- tests/test_repo.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/test_repo.py b/tests/test_repo.py index 0bcda17ee..5cacd9a1b 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -33,22 +33,20 @@ class TestRepo(unittest.TestCase): # Assert that the returned message is the expected one self.assertEqual(result, "a good commit message") - def test_get_commit_message_strip_quotes(self): - # Mock the IO object - mock_io = MagicMock() - - # Initialize the Coder object with the mocked IO and mocked repo - coder = Coder.create(models.GPT4, None, mock_io) - - # Mock the send method to set partial_response_content and return False - def mock_send(*args, **kwargs): - coder.partial_response_content = "a good commit message" - return False - - coder.send = MagicMock(side_effect=mock_send) + @patch("aider.repo.send_with_retries") + def test_get_commit_message_strip_quotes(self, mock_send): + # Set the return value of the mocked function + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = '"a good commit message"' + mock_send.return_value = ( + None, + mock_response + ) + repo = AiderRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context - result = coder.get_commit_message("dummy diff", "dummy context") + result = repo.get_commit_message("dummy diff", "dummy context") # Assert that the returned message is the expected one self.assertEqual(result, "a good commit message") From e9090f8d61d487e398666b3e7d231a49fafdf677 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:32:20 -0300 Subject: [PATCH 20/40] Refactored creating a Coder object and getting tracked files in test_repo.py. --- tests/test_repo.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/test_repo.py b/tests/test_repo.py index 5cacd9a1b..f711527b7 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -79,15 +79,8 @@ class TestRepo(unittest.TestCase): repo.git.commit("-m", "added") - # Create a Coder object on the temporary directory - coder = Coder.create( - models.GPT4, - None, - io=InputOutput(), - fnames=[str(tempdir / filenames[0])], - ) - tracked_files = coder.get_tracked_files() + tracked_files = AiderRepo(InputOutput(), [tempdir]).get_tracked_files() # On windows, paths will come back \like\this, so normalize them back to Paths tracked_files = [Path(fn) for fn in tracked_files] From 1fcb090ff6e90e2b13cac4bdcb4af787f8d76c4c Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 14:36:26 -0300 Subject: [PATCH 21/40] Refactor coder.get_tracked_files to coder.repo.get_tracked_files in test_coder.py and add test_get_commit_message_no_strip_unmatched_quotes in test_repo.py. --- tests/test_coder.py | 28 ++++------------------------ tests/test_repo.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/tests/test_coder.py b/tests/test_coder.py index e15870b84..fbfa8417d 100644 --- a/tests/test_coder.py +++ b/tests/test_coder.py @@ -53,7 +53,7 @@ class TestCoder(unittest.TestCase): # Mock the git repo mock = MagicMock() mock.return_value = set(["file1.txt", "file2.py"]) - coder.get_tracked_files = mock + coder.repo.get_tracked_files = mock # Call the check_for_file_mentions method coder.check_for_file_mentions("Please check file1.txt and file2.py") @@ -97,7 +97,7 @@ class TestCoder(unittest.TestCase): mock = MagicMock() mock.return_value = set(["file1.txt", "file2.py"]) - coder.get_tracked_files = mock + coder.repo.get_tracked_files = mock # Call the check_for_file_mentions method coder.check_for_file_mentions("Please check file1.txt and file2.py") @@ -128,7 +128,7 @@ class TestCoder(unittest.TestCase): mock = MagicMock() mock.return_value = set([str(fname), str(other_fname)]) - coder.get_tracked_files = mock + coder.repo.get_tracked_files = mock # Call the check_for_file_mentions method coder.check_for_file_mentions(f"Please check {fname}!") @@ -146,7 +146,7 @@ class TestCoder(unittest.TestCase): mock = MagicMock() mock.return_value = set([str(fname)]) - coder.get_tracked_files = mock + coder.repo.get_tracked_files = mock dump(fname) # Call the check_for_file_mentions method @@ -154,26 +154,6 @@ class TestCoder(unittest.TestCase): self.assertEqual(coder.abs_fnames, set([str(fname.resolve())])) - def test_get_commit_message_no_strip_unmatched_quotes(self): - # Mock the IO object - mock_io = MagicMock() - - # Initialize the Coder object with the mocked IO and mocked repo - coder = Coder.create(models.GPT4, None, mock_io) - - # Mock the send method to set partial_response_content and return False - def mock_send(*args, **kwargs): - coder.partial_response_content = 'a good "commit message"' - return False - - coder.send = MagicMock(side_effect=mock_send) - - # Call the get_commit_message method with dummy diff and context - result = coder.get_commit_message("dummy diff", "dummy context") - - # Assert that the returned message is the expected one - self.assertEqual(result, 'a good "commit message"') - def test_run_with_file_deletion(self): # Create a few temporary files diff --git a/tests/test_repo.py b/tests/test_repo.py index f711527b7..de59a4fe4 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -51,6 +51,25 @@ class TestRepo(unittest.TestCase): # Assert that the returned message is the expected one self.assertEqual(result, "a good commit message") + @patch("aider.repo.send_with_retries") + def test_get_commit_message_no_strip_unmatched_quotes(self, mock_send): + # Set the return value of the mocked function + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = 'a good "commit message"' + mock_send.return_value = ( + None, + mock_response + ) + + repo = AiderRepo(InputOutput(), None) + # Call the get_commit_message method with dummy diff and context + result = repo.get_commit_message("dummy diff", "dummy context") + + # Assert that the returned message is the expected one + self.assertEqual(result, 'a good "commit message"') + + def test_get_tracked_files(self): # Create a temporary directory tempdir = Path(tempfile.mkdtemp()) From 6d22dd06f80f253dc0db9962d4a1f3b9c3bf4ea6 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 15:56:27 -0300 Subject: [PATCH 22/40] aider: Refactored `test_main_with_empty_dir_new_file` to use `@patch` decorator to mock `get_commit_message` method. # Aider chat conversation: USER: @patch("aider.repo.AiderRepo.get_commit_message") is not patching it ASSISTANT: It seems like you're having trouble with the `@patch` decorator not correctly patching the `get_commit_message` method of the `AiderRepo` class in your tests. To help you with this, I need to understand the context better. Could you please provide more details about the issue? Are you getting any error messages? If so, could you share them? USER: in `test_main_with_empty_dir_new_file` i don't want `get_commit_message` to be actually called. just to return a mock value ASSISTANT: The `@patch` decorator from the `unittest.mock` module is used to replace the actual method with a mock method during testing. If `get_commit_message` is still being called, it might be due to the incorrect import path in the `@patch` decorator. In your case, the `@patch` decorator should be used as follows: ```python @patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!") ``` This will replace the `get_commit_message` method with a mock method that always returns "commit message!". Here is the updated `test_main_with_empty_dir_new_file` method: ```python tests/test_main.py <<<<<<< ORIGINAL @patch("aider.repo.AiderRepo.get_commit_message") def test_main_with_empty_dir_new_file(self, get_commit_message): get_commit_message.return_value = "commit message!" main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) ======= @patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!") def test_main_with_empty_dir_new_file(self, get_commit_message): main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) >>>>>>> UPDATED ``` This change should ensure that the actual `get_commit_message` method is not called during the test. Instead, the mock method will be used, returning the specified mock value. --- aider/main.py | 5 +++-- aider/repo.py | 17 +++++++++-------- tests/test_main.py | 5 +++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/aider/main.py b/aider/main.py index 3958f2d38..c6c5e6091 100644 --- a/aider/main.py +++ b/aider/main.py @@ -440,8 +440,6 @@ def main(args=None, input=None, output=None): io.tool_output(repo_map) return - coder.dirty_commit() - if args.apply: content = io.read_text(args.apply) if content is None: @@ -450,6 +448,9 @@ def main(args=None, input=None, output=None): return io.tool_output("Use /help to see in-chat commands, run with --help to see cmd line args") + + coder.dirty_commit() + if args.message: io.tool_output() coder.run(with_message=args.message) diff --git a/aider/repo.py b/aider/repo.py index 330827adf..0016c332d 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -129,6 +129,15 @@ class AiderRepo: return commit_message def get_diffs(self, pretty, *args): + try: + commits = self.repo.iter_commits(self.repo.active_branch) + current_branch_has_commits = any(commits) + except git.exc.GitCommandError: + current_branch_has_commits = False + + if not current_branch_has_commits: + return "" + if pretty: args = ["--color"] + list(args) if not args: @@ -138,14 +147,6 @@ class AiderRepo: return diffs def show_diffs(self, pretty): - try: - current_branch_has_commits = any(self.repo.iter_commits(self.repo.active_branch)) - except git.exc.GitCommandError: - current_branch_has_commits = False - - if not current_branch_has_commits: - return "" - diffs = self.get_diffs(pretty) print(diffs) diff --git a/tests/test_main.py b/tests/test_main.py index e85c8439d..d5bc80d10 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -34,8 +34,9 @@ class TestMain(TestCase): def test_main_with_empty_dir_no_files_on_command(self): main(["--no-git"], input=DummyInput(), output=DummyOutput()) - def test_main_with_empty_dir_new_file(self): - main(["foo.txt", "--yes"], input=DummyInput(), output=DummyOutput()) + @patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!") + def test_main_with_empty_dir_new_file(self, get_commit_message): + main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) def test_main_with_empty_git_dir_new_file(self): From dfb106d384ac3609cdfad7c1b550353c9822457f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 15:58:03 -0300 Subject: [PATCH 23/40] aider: Refactor the test_main_with_empty_dir_new_file function in tests/test_main.py to correctly mock the get_commit_message function. # Aider chat conversation: USER: I ran this command: pytest tests/test_main.py -k test_main_with_empty_git_dir_new_file And got this output: ============================= test session starts ============================== platform darwin -- Python 3.11.3, pytest-7.3.1, pluggy-1.0.0 rootdir: /Users/gauthier/Projects/aider configfile: pytest.ini collected 9 items / 8 deselected / 1 selected tests/test_main.py F [100%] =================================== FAILURES =================================== ________________ TestMain.test_main_with_empty_git_dir_new_file ________________ self = def test_main_with_empty_git_dir_new_file(self): make_repo() > main(["--yes", "foo.txt"], input=DummyInput(), output=DummyOutput()) tests/test_main.py:44: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ aider/main.py:452: in main coder.dirty_commit() aider/coders/base_coder.py:850: in dirty_commit self.repo.commit(message=message) aider/repo.py:64: in commit commit_message = self.get_commit_message(diffs, context) aider/repo.py:110: in get_commit_message _hash, response = send_with_retries( .venv/lib/python3.11/site-packages/backoff/_sync.py:105: in retry ret = target(*args, **kwargs) aider/sendchat.py:43: in send_with_retries res = openai.ChatCompletion.create(**kwargs) .venv/lib/python3.11/site-packages/openai/api_resources/chat_completion.py:25: in create return super().create(*args, **kwargs) .venv/lib/python3.11/site-packages/openai/api_resources/abstract/engine_api_resource.py:153: in create response, _, api_key = requestor.request( .venv/lib/python3.11/site-packages/openai/api_requestor.py:298: in request resp, got_stream = self._interpret_response(result, stream) .venv/lib/python3.11/site-packages/openai/api_requestor.py:700: in _interpret_response self._interpret_response_line( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = rbody = '{\n "error": {\n "message": "Incorrect API key provided: deadbeef. You can find your API key at https://pla...ys.",\n "type": "invalid_request_error",\n "param": null,\n "code": "invalid_api_key"\n }\n}\n' rcode = 401 rheaders = {'Date': 'Fri, 21 Jul 2023 18:57:18 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '258', ...F-Cache-Status': 'DYNAMIC', 'Server': 'cloudflare', 'CF-RAY': '7ea5ab3bde6d41e9-EWR', 'alt-svc': 'h3=":443"; ma=86400'} stream = False def _interpret_response_line( self, rbody: str, rcode: int, rheaders, stream: bool ) -> OpenAIResponse: # HTTP 204 response code does not have any content in the body. if rcode == 204: return OpenAIResponse(None, rheaders) if rcode == 503: raise error.ServiceUnavailableError( "The server is overloaded or not ready yet.", rbody, rcode, headers=rheaders, ) try: if 'text/plain' in rheaders.get('Content-Type', ''): data = rbody else: data = json.loads(rbody) except (JSONDecodeError, UnicodeDecodeError) as e: raise error.APIError( f"HTTP code {rcode} from API ({rbody})", rbody, rcode, headers=rheaders ) from e resp = OpenAIResponse(data, rheaders) # In the future, we might add a "status" parameter to errors # to better handle the "error while streaming" case. stream_error = stream and "error" in resp.data if stream_error or not 200 <= rcode < 300: > raise self.handle_error_response( rbody, rcode, resp.data, rheaders, stream_error=stream_error ) E openai.error.AuthenticationError: Incorrect API key provided: deadbeef. You can find your API key at https://platform.openai.com/account/api-keys. .venv/lib/python3.11/site-packages/openai/api_requestor.py:763: AuthenticationError ----------------------------- Captured stdout call ----------------------------- Aider v0.9.1-dev Add .aider* to .gitignore (recommended)? yes Added .aider* to .gitignore Model: gpt-4 Creating empty file foo.txt Git repo: .git Repo-map: universal-ctags using 1024 tokens Added foo.txt to the chat. Adding foo.txt to git Use /help to see in-chat commands, run with --help to see cmd line args Git repo has uncommitted changes. Commit before the chat proceeds [y/n/commit message]? yes =========================== short test summary info ============================ FAILED tests/test_main.py::TestMain::test_main_with_empty_git_dir_new_file - ... ======================= 1 failed, 8 deselected in 1.53s ======================== ASSISTANT: The error message indicates that the API key provided ("deadbeef") is incorrect. You need to provide a valid OpenAI API key. The API key is set in the `setUp ^C KeyboardInterrupt USER: it's not supposed to be calling the real `get_commit_message` ASSISTANT: I see, the test is supposed to mock the `get_commit_message` function, but it seems like the real function is being called instead. This could be due to the mock not being applied correctly. In the `test_main_with_empty_git_dir_new_file` function in the `tests/test_main.py` file, the `get_commit_message` function is being patched. However, it seems like the patch is not being applied correctly. Let's modify the test to ensure that the `get_commit_message` function is being mocked correctly. Here is the change: ```python tests/test_main.py <<<<<<< ORIGINAL @patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!") def test_main_with_empty_dir_new_file(self, get_commit_message): main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) ======= @patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!") def test_main_with_empty_dir_new_file(self, _): main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) >>>>>>> UPDATED ``` In the updated code, I've replaced `get_commit_message` with `_` in the function arguments. This is a common convention in Python when the variable is not going to be used. This change ensures that the mock object is not being confused with the actual function. --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index d5bc80d10..3a522cb82 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -35,7 +35,7 @@ class TestMain(TestCase): main(["--no-git"], input=DummyInput(), output=DummyOutput()) @patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!") - def test_main_with_empty_dir_new_file(self, get_commit_message): + def test_main_with_empty_dir_new_file(self, _): main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) From c4a07f1f844510b351c5aaf8cb63484527b8edac Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 15:59:13 -0300 Subject: [PATCH 24/40] aider: The real `get_commit_message` is being replaced with a mock function in the `test_main_with_empty_git_dir_new_file` test. # Aider chat conversation: USER: the real `get_commit_message` is being called in `test_main_with_empty_git_dir_new_file` ASSISTANT: To prevent the real `get_commit_message` from being called during the test `test_main_with_empty_git_dir_new_file`, we can use the `unittest.mock.patch` decorator to replace it with a mock function. Here's how to do it: ```python tests/test_main.py <<<<<<< ORIGINAL def test_main_with_empty_git_dir_new_file(self): make_repo() main(["--yes", "foo.txt"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) ======= @patch("aider.repo.AiderRepo.get_commit_message", return_value="mock commit message") def test_main_with_empty_git_dir_new_file(self, _): make_repo() main(["--yes", "foo.txt"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) >>>>>>> UPDATED ``` This change will replace the `get_commit_message` method with a mock that always returns "mock commit message" during the execution of `test_main_with_empty_git_dir_new_file`. --- tests/test_main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 3a522cb82..d45f32c73 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -39,7 +39,8 @@ class TestMain(TestCase): main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) - def test_main_with_empty_git_dir_new_file(self): + @patch("aider.repo.AiderRepo.get_commit_message", return_value="mock commit message") + def test_main_with_empty_git_dir_new_file(self, _): make_repo() main(["--yes", "foo.txt"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) From 7c1e3c1b6a0b39dda27516f17d8e8e716c1b6b02 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 16:00:19 -0300 Subject: [PATCH 25/40] cleanup --- tests/test_main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index d45f32c73..8b08c61b1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -34,9 +34,8 @@ class TestMain(TestCase): def test_main_with_empty_dir_no_files_on_command(self): main(["--no-git"], input=DummyInput(), output=DummyOutput()) - @patch("aider.repo.AiderRepo.get_commit_message", return_value="commit message!") - def test_main_with_empty_dir_new_file(self, _): - main(["foo.txt", "--yes","--no-git"], input=DummyInput(), output=DummyOutput()) + def test_main_with_empty_dir_new_file(self): + main(["foo.txt", "--yes", "--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) @patch("aider.repo.AiderRepo.get_commit_message", return_value="mock commit message") From 9f8ea93ce6f5a3f776033707409cea4e78111967 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 16:08:27 -0300 Subject: [PATCH 26/40] AiderRepo -> GitRepo --- aider/coders/base_coder.py | 4 ++-- aider/repo.py | 2 +- tests/test_main.py | 2 +- tests/test_repo.py | 30 ++++++++---------------------- 4 files changed, 12 insertions(+), 26 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 585e208a6..e54fb1087 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -17,7 +17,7 @@ from rich.markdown import Markdown from aider import models, prompts, utils from aider.commands import Commands -from aider.repo import AiderRepo +from aider.repo import GitRepo from aider.repomap import RepoMap from aider.sendchat import send_with_retries @@ -159,7 +159,7 @@ class Coder: if use_git: try: - self.repo = AiderRepo(self.io, fnames) + self.repo = GitRepo(self.io, fnames) self.root = self.repo.root except FileNotFoundError: self.repo = None diff --git a/aider/repo.py b/aider/repo.py index 0016c332d..0b01e9978 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -10,7 +10,7 @@ from aider.sendchat import send_with_retries from .dump import dump # noqa: F401 -class AiderRepo: +class GitRepo: repo = None def __init__(self, io, fnames): diff --git a/tests/test_main.py b/tests/test_main.py index 8b08c61b1..6bda03c7f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -38,7 +38,7 @@ class TestMain(TestCase): main(["foo.txt", "--yes", "--no-git"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) - @patch("aider.repo.AiderRepo.get_commit_message", return_value="mock commit message") + @patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message") def test_main_with_empty_git_dir_new_file(self, _): make_repo() main(["--yes", "foo.txt"], input=DummyInput(), output=DummyOutput()) diff --git a/tests/test_repo.py b/tests/test_repo.py index de59a4fe4..ca0985886 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -5,13 +5,10 @@ from pathlib import Path from unittest.mock import MagicMock, patch import git -import openai -from aider import models from aider.dump import dump # noqa: F401 from aider.io import InputOutput -from aider.repo import AiderRepo -from tests.utils import GitTemporaryDirectory +from aider.repo import GitRepo class TestRepo(unittest.TestCase): @@ -21,12 +18,9 @@ class TestRepo(unittest.TestCase): mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message.content = "a good commit message" - mock_send.return_value = ( - None, - mock_response - ) + mock_send.return_value = (None, mock_response) - repo = AiderRepo(InputOutput(), None) + repo = GitRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context result = repo.get_commit_message("dummy diff", "dummy context") @@ -39,12 +33,9 @@ class TestRepo(unittest.TestCase): mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message.content = '"a good commit message"' - mock_send.return_value = ( - None, - mock_response - ) + mock_send.return_value = (None, mock_response) - repo = AiderRepo(InputOutput(), None) + repo = GitRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context result = repo.get_commit_message("dummy diff", "dummy context") @@ -57,19 +48,15 @@ class TestRepo(unittest.TestCase): mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message.content = 'a good "commit message"' - mock_send.return_value = ( - None, - mock_response - ) + mock_send.return_value = (None, mock_response) - repo = AiderRepo(InputOutput(), None) + repo = GitRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context result = repo.get_commit_message("dummy diff", "dummy context") # Assert that the returned message is the expected one self.assertEqual(result, 'a good "commit message"') - def test_get_tracked_files(self): # Create a temporary directory tempdir = Path(tempfile.mkdtemp()) @@ -98,8 +85,7 @@ class TestRepo(unittest.TestCase): repo.git.commit("-m", "added") - - tracked_files = AiderRepo(InputOutput(), [tempdir]).get_tracked_files() + tracked_files = GitRepo(InputOutput(), [tempdir]).get_tracked_files() # On windows, paths will come back \like\this, so normalize them back to Paths tracked_files = [Path(fn) for fn in tracked_files] From e2a32fec7e8327b2dac652ef6d3ef6f93d145676 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 16:08:50 -0300 Subject: [PATCH 27/40] linting --- scripts/versionbump.py | 4 +++- tests/test_coder.py | 1 - tests/test_repomap.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/versionbump.py b/scripts/versionbump.py index 027e15658..52ba19173 100644 --- a/scripts/versionbump.py +++ b/scripts/versionbump.py @@ -19,7 +19,9 @@ def main(): raise ValueError(f"Invalid version format, must be x.y.z: {new_version_str}") new_version = version.parse(new_version_str) - incremented_version = version.Version(f"{new_version.major}.{new_version.minor}.{new_version.micro + 1}") + incremented_version = version.Version( + f"{new_version.major}.{new_version.minor}.{new_version.micro + 1}" + ) with open("aider/__init__.py", "r") as f: content = f.read() diff --git a/tests/test_coder.py b/tests/test_coder.py index 856bd9abc..a9ffaeca5 100644 --- a/tests/test_coder.py +++ b/tests/test_coder.py @@ -1,4 +1,3 @@ -import os import tempfile import unittest from pathlib import Path diff --git a/tests/test_repomap.py b/tests/test_repomap.py index 1ecc951b2..82c100468 100644 --- a/tests/test_repomap.py +++ b/tests/test_repomap.py @@ -4,7 +4,6 @@ from unittest.mock import patch from aider.io import InputOutput from aider.repomap import RepoMap - from tests.utils import IgnorantTemporaryDirectory From 661a521693628385616ed6413113453fe288e5d4 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 21 Jul 2023 16:20:27 -0300 Subject: [PATCH 28/40] aider.repo.simple_send_with_retries --- aider/repo.py | 16 +++------------- aider/sendchat.py | 13 +++++++++++++ tests/test_repo.py | 26 +++++++------------------- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/aider/repo.py b/aider/repo.py index 0b01e9978..5ae06baed 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -2,10 +2,9 @@ import os from pathlib import Path, PurePosixPath import git -import openai from aider import models, prompts, utils -from aider.sendchat import send_with_retries +from aider.sendchat import simple_send_with_retries from .dump import dump # noqa: F401 @@ -104,19 +103,10 @@ class GitRepo: dict(role="user", content=content), ] - commit_message = None for model in [models.GPT35.name, models.GPT35_16k.name]: - try: - _hash, response = send_with_retries( - model=models.GPT35.name, - messages=messages, - functions=None, - stream=False, - ) - commit_message = response.choices[0].message.content + commit_message = simple_send_with_retries(model, messages) + if commit_message: break - except (AttributeError, openai.error.InvalidRequestError): - pass if not commit_message: self.io.tool_error("Failed to generate commit message!") diff --git a/aider/sendchat.py b/aider/sendchat.py index 9a4414230..441447863 100644 --- a/aider/sendchat.py +++ b/aider/sendchat.py @@ -42,3 +42,16 @@ def send_with_retries(model, messages, functions, stream): res = openai.ChatCompletion.create(**kwargs) return hash_object, res + + +def simple_send_with_retries(model, messages): + try: + _hash, response = send_with_retries( + model=model, + messages=messages, + functions=None, + stream=False, + ) + return response.choices[0].message.content + except (AttributeError, openai.error.InvalidRequestError): + return diff --git a/tests/test_repo.py b/tests/test_repo.py index ca0985886..67983a134 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -2,7 +2,7 @@ import os import tempfile import unittest from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import patch import git @@ -12,13 +12,9 @@ from aider.repo import GitRepo class TestRepo(unittest.TestCase): - @patch("aider.repo.send_with_retries") + @patch("aider.repo.simple_send_with_retries") def test_get_commit_message(self, mock_send): - # Set the return value of the mocked function - mock_response = MagicMock() - mock_response.choices = [MagicMock()] - mock_response.choices[0].message.content = "a good commit message" - mock_send.return_value = (None, mock_response) + mock_send.return_value = "a good commit message" repo = GitRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context @@ -27,13 +23,9 @@ class TestRepo(unittest.TestCase): # Assert that the returned message is the expected one self.assertEqual(result, "a good commit message") - @patch("aider.repo.send_with_retries") + @patch("aider.repo.simple_send_with_retries") def test_get_commit_message_strip_quotes(self, mock_send): - # Set the return value of the mocked function - mock_response = MagicMock() - mock_response.choices = [MagicMock()] - mock_response.choices[0].message.content = '"a good commit message"' - mock_send.return_value = (None, mock_response) + mock_send.return_value = '"a good commit message"' repo = GitRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context @@ -42,13 +34,9 @@ class TestRepo(unittest.TestCase): # Assert that the returned message is the expected one self.assertEqual(result, "a good commit message") - @patch("aider.repo.send_with_retries") + @patch("aider.repo.simple_send_with_retries") def test_get_commit_message_no_strip_unmatched_quotes(self, mock_send): - # Set the return value of the mocked function - mock_response = MagicMock() - mock_response.choices = [MagicMock()] - mock_response.choices[0].message.content = 'a good "commit message"' - mock_send.return_value = (None, mock_response) + mock_send.return_value = 'a good "commit message"' repo = GitRepo(InputOutput(), None) # Call the get_commit_message method with dummy diff and context From 9ab21d36a636f4ee0386c06594cc7f55839e4245 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 22 Jul 2023 09:47:54 -0300 Subject: [PATCH 29/40] bugfix --- aider/coders/base_coder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index e54fb1087..d4bdcf504 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -799,7 +799,7 @@ class Coder: def auto_commit(self): context = self.get_context_from_history(self.cur_messages) res = self.repo.commit(context=context, prefix="aider: ") - if not res: + if res: commit_hash, commit_message = res self.last_aider_commit_hash = commit_hash From 85166341d18553c72a28f1db3adb25097431b018 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 13:03:38 -0300 Subject: [PATCH 30/40] aider: Add staged files to the list of tracked files in the `get_tracked_files` method. # Aider chat conversation: USER: doing commit.tree.traverse() is not picking up files which have been git added but not yet committed ASSISTANT: To include files that have been staged but not yet committed, you need to use the `git diff --cached` command. This command shows the changes between the index and your last commit; in other words, what you would be committing if you run "git commit" without "-a" option. However, this change requires modification of the `aider/repo.py` file, which is currently read-only. Could you please make it read-write so that I can propose the necessary changes? USER: I added these *read-write* files: aider/repo.py ASSISTANT: Great, now that we have write access to `aider/repo.py`, we can modify the `get_tracked_files` method to include files that have been staged but not yet committed. Here are the steps: 1. We will use the `git diff --cached --name-only` command to get a list of staged files. This command lists the file names that have been added (staged) but not yet committed. 2. We will add these staged files to the list of tracked files. Here is the *edit block* for the changes: ```python aider/repo.py <<<<<<< ORIGINAL def get_tracked ^C KeyboardInterrupt USER: is there a way to use traverse? ASSISTANT: Yes, you can use the `git.IndexFile` object to traverse the staged files. The `IndexFile` object represents the git index, which is where files are staged before they are committed. Here are the steps: 1. Get the `IndexFile` object from the `Repo` object. 2. Traverse the `IndexFile` object to get the staged files. Here is the *edit block* for the changes: ```python aider/repo.py <<<<<<< ORIGINAL def get_tracked_files(self): if not self.repo: return [] try: commit = self.repo.head.commit except ValueError: return set() files = [] for blob in commit.tree.traverse(): if blob.type == "blob": # blob is a file files.append(blob.path) # convert to appropriate os.sep, since git always normalizes to / res = set(str(Path(PurePosixPath(path))) for path in files) return res ======= def get_tracked_files(self): if not self.repo: return [] try: commit = self.repo.head.commit except ValueError: return set() files = [] for blob in commit.tree.traverse(): if blob.type == "blob": # blob is a file files.append(blob.path) # Add staged files index = self.repo.index staged_files = [item.a_path for item in index.diff("HEAD")] files.extend(staged_files) # convert to appropriate os.sep, since git always normalizes to / res = set(str(Path(PurePosixPath(path))) for path in files) return res >>>>>>> UPDATED ``` This change will ensure that the `get_tracked_files` method returns both committed and staged files. --- aider/repo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aider/repo.py b/aider/repo.py index 5ae06baed..0615e3b39 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -154,6 +154,11 @@ class GitRepo: if blob.type == "blob": # blob is a file files.append(blob.path) + # Add staged files + index = self.repo.index + staged_files = [item.a_path for item in index.diff("HEAD")] + files.extend(staged_files) + # convert to appropriate os.sep, since git always normalizes to / res = set(str(Path(PurePosixPath(path))) for path in files) From bef1d00ab121487e9574ddd87b468ecb44df2e09 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 13:42:05 -0300 Subject: [PATCH 31/40] Refactor test_get_tracked_files_with_new_staged_file to use a GitTemporaryDirectory and add new assertions. --- tests/test_repo.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/test_repo.py b/tests/test_repo.py index 67983a134..0290cf1e6 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -2,14 +2,14 @@ import os import tempfile import unittest from pathlib import Path -from unittest.mock import patch +from unittest.mock import patch, MagicMock import git from aider.dump import dump # noqa: F401 from aider.io import InputOutput from aider.repo import GitRepo - +from tests.utils import GitTemporaryDirectory class TestRepo(unittest.TestCase): @patch("aider.repo.simple_send_with_retries") @@ -80,3 +80,37 @@ class TestRepo(unittest.TestCase): # Assert that coder.get_tracked_files() returns the three filenames self.assertEqual(set(tracked_files), set(created_files)) + + def test_get_tracked_files_with_new_staged_file(self): + # Mock the IO object + mock_io = MagicMock() + + with GitTemporaryDirectory(): + # new repo + repo = git.Repo() + + # add it, but no commits at all in the repo yet + fname = Path("new.txt") + fname.touch() + repo.git.add(str(fname)) + + coder = GitRepo(InputOutput(), None) + + # better be there + fnames = coder.get_tracked_files() + self.assertIn(str(fname), fnames) + + # commit it, better still be there + repo.git.commit("-m", "new") + fnames = coder.get_tracked_files() + self.assertIn(str(fname), fnames) + + # new file, added but not committed + fname2 = Path("new2.txt") + fname2.touch() + repo.git.add(str(fname2)) + + # both should be there + fnames = coder.get_tracked_files() + self.assertIn(str(fname), fnames) + self.assertIn(str(fname2), fnames) From 89542d2139dbc822d7d4539f72191b01ad00caa6 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 13:43:05 -0300 Subject: [PATCH 32/40] aider: Handle the case where the repository has never had any commits by using `index.diff(None)` to get the staged files. # Aider chat conversation: USER: index.diff("HEAD") fails when the repo has never had any commits ASSISTANT: To handle the case where the repository has never had any commits, we can add a try-except block around the `index.diff("HEAD")` call in the `get_tracked_files` method of the `GitRepo` class in the `aider/repo.py` file. If a `git.exc.BadName` exception is raised, we can assume that there are no commits and return an empty list for the staged files. Here is the proposed change: ```python aider/repo.py <<<<<<< ORIGINAL # Add staged files index = self.repo.index staged_files = [item.a_path for item in index.diff("HEAD")] files.extend(staged_files) ======= # Add staged files ^C KeyboardInterrupt USER: there can be staged files when there are not commits ASSISTANT: You're correct. If there are no commits, `index.diff("HEAD")` will fail, but there can still be staged files. We can use `index.diff(None)` to get the staged files in this case. Here's the proposed change: ```python aider/repo.py <<<<<<< ORIGINAL # Add staged files index = self.repo.index staged_files = [item.a_path for item in index.diff("HEAD")] files.extend(staged_files) ======= # Add staged files index = self.repo.index try: staged_files = [item.a_path for item in index.diff("HEAD")] except git.exc.BadName: staged_files = [item.a_path for item in index.diff(None)] files.extend(staged_files) >>>>>>> UPDATED ``` --- aider/repo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aider/repo.py b/aider/repo.py index 0615e3b39..15e89a54f 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -156,7 +156,10 @@ class GitRepo: # Add staged files index = self.repo.index - staged_files = [item.a_path for item in index.diff("HEAD")] + try: + staged_files = [item.a_path for item in index.diff("HEAD")] + except git.exc.BadName: + staged_files = [item.a_path for item in index.diff(None)] files.extend(staged_files) # convert to appropriate os.sep, since git always normalizes to / From f87ec8c38bea3dcc6310549dbb44d8726eeddb21 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 13:44:22 -0300 Subject: [PATCH 33/40] Refactor getting staged files to handle ValueError. --- aider/repo.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/aider/repo.py b/aider/repo.py index 15e89a54f..dff2a8543 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -147,19 +147,18 @@ class GitRepo: try: commit = self.repo.head.commit except ValueError: - return set() + commit = None files = [] - for blob in commit.tree.traverse(): - if blob.type == "blob": # blob is a file - files.append(blob.path) + if commit: + for blob in commit.tree.traverse(): + if blob.type == "blob": # blob is a file + files.append(blob.path) # Add staged files index = self.repo.index - try: - staged_files = [item.a_path for item in index.diff("HEAD")] - except git.exc.BadName: - staged_files = [item.a_path for item in index.diff(None)] + staged_files = [item.a_path for item in index.diff(None)] + dump(staged_files) files.extend(staged_files) # convert to appropriate os.sep, since git always normalizes to / From df096272bc791bf4dc060d33a47287e7caede8c4 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 13:45:10 -0300 Subject: [PATCH 34/40] aider: Refactored `get_tracked_files` method to use `index.entries` instead of `index.diff` to enumerate staged files. # Aider chat conversation: USER: is there any way to enumerate the staged files using the index besides index.diff ASSISTANT: Yes, you can enumerate the staged files using the `index.entries` property of the `IndexFile` object. This property returns a dictionary-like object where the keys are the file paths and the values are `IndexEntry` objects. Here's how you can modify the `get_tracked_files` method in the `GitRepo` class to use `index.entries` instead of `index.diff`: ```python aider/repo.py <<<<<<< ORIGINAL def get_tracked_files(self): if not self.repo: return [] try: commit = self.repo.head.commit except ValueError: commit = None files = [] if commit: for blob in commit.tree.traverse(): if blob.type == "blob": # blob is a file files.append(blob.path) # Add staged files index = self.repo.index staged_files = [item.a_path for item in index.diff(None)] dump(staged_files) files.extend(staged_files) # convert to appropriate os.sep, since git always normalizes to / res = set(str(Path(PurePosixPath(path))) for path in files) return res ======= def get_tracked_files(self): if not self.repo: return [] try: commit = self.repo.head.commit except ValueError: commit = None files = [] if commit: for blob in commit.tree.traverse(): if blob.type == "blob": # blob is a file files.append(blob.path) # Add staged files index = self.repo.index staged_files = [str(Path(PurePosixPath(path))) for path in index.entries.keys()] dump(staged_files) files.extend(staged_files) # convert to appropriate os.sep, since git always normalizes to / res = set(str(Path(PurePosixPath(path))) for path in files) return res >>>>>>> UPDATED ``` This change will make the `get_tracked_files` method return all files in the index, not just the ones that have been modified. If you only want to get the files that have been modified, you should stick with using `index.diff`. --- aider/repo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/repo.py b/aider/repo.py index dff2a8543..c8601a97c 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -157,7 +157,7 @@ class GitRepo: # Add staged files index = self.repo.index - staged_files = [item.a_path for item in index.diff(None)] + staged_files = [str(Path(PurePosixPath(path))) for path in index.entries.keys()] dump(staged_files) files.extend(staged_files) From 66da82094d317fc37c43d29e8a1a282a57966d2c Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 13:47:33 -0300 Subject: [PATCH 35/40] use index.entries --- aider/repo.py | 4 ++-- tests/test_repo.py | 24 +++++++++++------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/aider/repo.py b/aider/repo.py index c8601a97c..473234c20 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -157,8 +157,8 @@ class GitRepo: # Add staged files index = self.repo.index - staged_files = [str(Path(PurePosixPath(path))) for path in index.entries.keys()] - dump(staged_files) + staged_files = [path for path, _ in index.entries.keys()] + files.extend(staged_files) # convert to appropriate os.sep, since git always normalizes to / diff --git a/tests/test_repo.py b/tests/test_repo.py index 0290cf1e6..d96355ceb 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -2,7 +2,7 @@ import os import tempfile import unittest from pathlib import Path -from unittest.mock import patch, MagicMock +from unittest.mock import patch import git @@ -11,6 +11,7 @@ from aider.io import InputOutput from aider.repo import GitRepo from tests.utils import GitTemporaryDirectory + class TestRepo(unittest.TestCase): @patch("aider.repo.simple_send_with_retries") def test_get_commit_message(self, mock_send): @@ -82,35 +83,32 @@ class TestRepo(unittest.TestCase): self.assertEqual(set(tracked_files), set(created_files)) def test_get_tracked_files_with_new_staged_file(self): - # Mock the IO object - mock_io = MagicMock() - with GitTemporaryDirectory(): # new repo - repo = git.Repo() + raw_repo = git.Repo() - # add it, but no commits at all in the repo yet + # add it, but no commits at all in the raw_repo yet fname = Path("new.txt") fname.touch() - repo.git.add(str(fname)) + raw_repo.git.add(str(fname)) - coder = GitRepo(InputOutput(), None) + git_repo = GitRepo(InputOutput(), None) # better be there - fnames = coder.get_tracked_files() + fnames = git_repo.get_tracked_files() self.assertIn(str(fname), fnames) # commit it, better still be there - repo.git.commit("-m", "new") - fnames = coder.get_tracked_files() + raw_repo.git.commit("-m", "new") + fnames = git_repo.get_tracked_files() self.assertIn(str(fname), fnames) # new file, added but not committed fname2 = Path("new2.txt") fname2.touch() - repo.git.add(str(fname2)) + raw_repo.git.add(str(fname2)) # both should be there - fnames = coder.get_tracked_files() + fnames = git_repo.get_tracked_files() self.assertIn(str(fname), fnames) self.assertIn(str(fname2), fnames) From 64c50bab48a3e963aafca6601196119bda49fda8 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 17:31:00 -0300 Subject: [PATCH 36/40] Accept either a git dname or a list of fnames --- aider/coders/base_coder.py | 8 ++- aider/main.py | 117 +++++++++++++++++++++++++++++-------- aider/repo.py | 11 +++- 3 files changed, 109 insertions(+), 27 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index d4bdcf504..6fc81c8f2 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -98,6 +98,7 @@ class Coder: main_model, io, fnames=None, + git_dname=None, pretty=True, show_diffs=False, auto_commits=True, @@ -155,11 +156,14 @@ class Coder: fname.parent.mkdir(parents=True, exist_ok=True) fname.touch() + if not fname.is_file(): + raise ValueError(f"{fname} is not a file") + self.abs_fnames.add(str(fname.resolve())) if use_git: try: - self.repo = GitRepo(self.io, fnames) + self.repo = GitRepo(self.io, fnames, git_dname) self.root = self.repo.root except FileNotFoundError: self.repo = None @@ -197,7 +201,7 @@ class Coder: self.io.tool_output(f"Added {fname} to the chat.") if self.repo: - self.repo.add_new_files(fnames) + self.repo.add_new_files(fname for fname in fnames if not Path(fname).is_dir()) # validate the functions jsonschema if self.functions: diff --git a/aider/main.py b/aider/main.py index 1eba45db1..f8bcae55b 100644 --- a/aider/main.py +++ b/aider/main.py @@ -9,10 +9,14 @@ import openai from aider import __version__, models from aider.coders import Coder from aider.io import InputOutput +from aider.repo import GitRepo from aider.versioncheck import check_version +from .dump import dump # noqa: F402 + def get_git_root(): + """Try and guess the git repo, since the conf.yml can be at the repo root""" try: repo = git.Repo(search_parent_directories=True) return repo.working_tree_dir @@ -20,6 +24,33 @@ def get_git_root(): return None +def guessed_wrong_repo(io, git_root, fnames, git_dname): + """After we parse the args, we can determine the real repo. Did we guess wrong?""" + + dump(git_root) + + try: + check_repo = Path(GitRepo(io, fnames, git_dname).root).resolve() + except FileNotFoundError: + return + + dump(check_repo) + + # we had no guess, rely on the "true" repo result + if not git_root: + return str(check_repo) + + git_root = Path(git_root).resolve() + if check_repo == git_root: + return + + print("guessed the wrong repo") + dump(check_repo) + dump(git_root) + + return str(check_repo) + + def setup_git(git_root, io): if git_root: return git_root @@ -71,11 +102,14 @@ def check_gitignore(git_root, io, ask=True): io.tool_output(f"Added {pat} to .gitignore") -def main(args=None, input=None, output=None): - if args is None: - args = sys.argv[1:] +def main(argv=None, input=None, output=None, force_git_root=None): + if argv is None: + argv = sys.argv[1:] - git_root = get_git_root() + if force_git_root: + git_root = force_git_root + else: + git_root = get_git_root() conf_fname = Path(".aider.conf.yml") @@ -344,7 +378,7 @@ def main(args=None, input=None, output=None): ), ) - args = parser.parse_args(args) + args = parser.parse_args(argv) if args.dark_mode: args.user_input_color = "#32FF32" @@ -371,6 +405,38 @@ def main(args=None, input=None, output=None): dry_run=args.dry_run, ) + fnames = args.files + if len(args.files) > 1: + good = True + for fname in args.files: + if Path(fname).is_dir(): + io.tool_error(f"{fname} is a directory, not provided alone.") + good = False + if not good: + io.tool_error( + "Provide either a single directory of a git repo, or a list of one or more files." + ) + return 1 + + git_dname = None + if len(args.files) == 1: + if Path(args.files[0]).is_dir(): + if args.git: + git_dname = args.files[0] + fnames = [] + else: + io.tool_error(f"{args.files[0]} is a directory, but --no-git selected.") + return -1 + + # We can't know the git repo for sure until after parsing the args. + # If we guessed wrong, reparse because that changes things like + # the location of the config.yml and history files. + if args.git and not force_git_root: + right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname) + if right_repo_root: + print("guessed wrong") + return main(argv, input, output, right_repo_root) + io.tool_output(f"Aider v{__version__}") check_version(io.tool_error) @@ -418,24 +484,29 @@ def main(args=None, input=None, output=None): setattr(openai, mod_key, val) io.tool_output(f"Setting openai.{mod_key}={val}") - coder = Coder.create( - main_model, - args.edit_format, - io, - ## - fnames=args.files, - pretty=args.pretty, - show_diffs=args.show_diffs, - auto_commits=args.auto_commits, - dirty_commits=args.dirty_commits, - dry_run=args.dry_run, - map_tokens=args.map_tokens, - verbose=args.verbose, - assistant_output_color=args.assistant_output_color, - code_theme=args.code_theme, - stream=args.stream, - use_git=args.git, - ) + try: + coder = Coder.create( + main_model, + args.edit_format, + io, + ## + fnames=fnames, + git_dname=git_dname, + pretty=args.pretty, + show_diffs=args.show_diffs, + auto_commits=args.auto_commits, + dirty_commits=args.dirty_commits, + dry_run=args.dry_run, + map_tokens=args.map_tokens, + verbose=args.verbose, + assistant_output_color=args.assistant_output_color, + code_theme=args.code_theme, + stream=args.stream, + use_git=args.git, + ) + except ValueError as err: + io.tool_error(str(err)) + return if args.show_repo_map: repo_map = coder.get_repo_map() diff --git a/aider/repo.py b/aider/repo.py index 473234c20..16bcc45e4 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -12,10 +12,12 @@ from .dump import dump # noqa: F401 class GitRepo: repo = None - def __init__(self, io, fnames): + def __init__(self, io, fnames, git_dname): self.io = io - if fnames: + if git_dname: + check_fnames = [git_dname] + elif fnames: check_fnames = fnames else: check_fnames = ["."] @@ -25,6 +27,9 @@ class GitRepo: fname = Path(fname) fname = fname.resolve() + if not fname.exists() and fname.parent.exists(): + fname = fname.parent + try: repo_path = git.Repo(fname, search_parent_directories=True).working_dir repo_path = utils.safe_abs_path(repo_path) @@ -49,6 +54,8 @@ class GitRepo: for fname in fnames: if Path(fname).resolve() in cur_files: continue + if not Path(fname).exists(): + continue self.io.tool_output(f"Adding {fname} to git") self.repo.git.add(fname) From cac627ae5f9b92ea127eaf6dd338517a53822c1a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 17:32:09 -0300 Subject: [PATCH 37/40] aider: Add `None` as the third argument in all calls to `GitRepo`. # Aider chat conversation: USER: `GitRepo` now takes a 3rd arg. add None as the missing arg in all calls ASSISTANT: To add `None` as the third argument in all calls to `GitRepo`, we need to modify the `tests/test_repo.py` file. Here are the changes: ```python tests/test_repo.py <<<<<<< ORIGINAL repo = GitRepo(InputOutput(), None) ======= repo = GitRepo(InputOutput(), None, None) >>>>>>> UPDATED ``` ```python tests/test_repo.py <<<<<<< ORIGINAL repo = GitRepo(InputOutput(), None) ======= repo = GitRepo(InputOutput(), None, None) >>>>>>> UPDATED ``` ```python tests/test_repo.py <<<<<<< ORIGINAL repo = GitRepo(InputOutput(), None) ======= repo = GitRepo(InputOutput(), None, None) >>>>>>> UPDATED ``` ```python tests/test_repo.py <<<<<<< ORIGINAL tracked_files = GitRepo(InputOutput(), [tempdir]).get_tracked_files() ======= tracked_files = GitRepo(InputOutput(), [tempdir], None).get_tracked_files() >>>>>>> UPDATED ``` ```python tests/test_repo.py <<<<<<< ORIGINAL git_repo = GitRepo(InputOutput(), None) ======= git_repo = GitRepo(InputOutput(), None, None) >>>>>>> UPDATED ``` --- tests/test_repo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_repo.py b/tests/test_repo.py index d96355ceb..488959a09 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -17,7 +17,7 @@ class TestRepo(unittest.TestCase): def test_get_commit_message(self, mock_send): mock_send.return_value = "a good commit message" - repo = GitRepo(InputOutput(), None) + repo = GitRepo(InputOutput(), None, None) # Call the get_commit_message method with dummy diff and context result = repo.get_commit_message("dummy diff", "dummy context") @@ -28,7 +28,7 @@ class TestRepo(unittest.TestCase): def test_get_commit_message_strip_quotes(self, mock_send): mock_send.return_value = '"a good commit message"' - repo = GitRepo(InputOutput(), None) + repo = GitRepo(InputOutput(), None, None) # Call the get_commit_message method with dummy diff and context result = repo.get_commit_message("dummy diff", "dummy context") @@ -39,7 +39,7 @@ class TestRepo(unittest.TestCase): def test_get_commit_message_no_strip_unmatched_quotes(self, mock_send): mock_send.return_value = 'a good "commit message"' - repo = GitRepo(InputOutput(), None) + repo = GitRepo(InputOutput(), None, None) # Call the get_commit_message method with dummy diff and context result = repo.get_commit_message("dummy diff", "dummy context") @@ -74,7 +74,7 @@ class TestRepo(unittest.TestCase): repo.git.commit("-m", "added") - tracked_files = GitRepo(InputOutput(), [tempdir]).get_tracked_files() + tracked_files = GitRepo(InputOutput(), [tempdir], None).get_tracked_files() # On windows, paths will come back \like\this, so normalize them back to Paths tracked_files = [Path(fn) for fn in tracked_files] @@ -92,7 +92,7 @@ class TestRepo(unittest.TestCase): fname.touch() raw_repo.git.add(str(fname)) - git_repo = GitRepo(InputOutput(), None) + git_repo = GitRepo(InputOutput(), None, None) # better be there fnames = git_repo.get_tracked_files() From 957f59c01163757d04610e338d640b8d1965e930 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 17:35:47 -0300 Subject: [PATCH 38/40] updated help --- aider/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/main.py b/aider/main.py index f8bcae55b..a3bf80b1c 100644 --- a/aider/main.py +++ b/aider/main.py @@ -135,7 +135,7 @@ def main(argv=None, input=None, output=None, force_git_root=None): "files", metavar="FILE", nargs="*", - help="a list of source code files to edit with GPT (optional)", + help="the directory of a git repo, or a list of files to edit with GPT (optional)", ) core_group.add_argument( "--openai-api-key", From c8f1c0609d311fc21c2f9e175aad9f3188a5a01b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 24 Jul 2023 17:47:53 -0300 Subject: [PATCH 39/40] tests and bugfix --- aider/main.py | 8 ++++---- tests/test_main.py | 27 +++++++++++++++++++++++++++ tests/utils.py | 8 ++++++-- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/aider/main.py b/aider/main.py index a3bf80b1c..97efe8bd5 100644 --- a/aider/main.py +++ b/aider/main.py @@ -405,7 +405,7 @@ def main(argv=None, input=None, output=None, force_git_root=None): dry_run=args.dry_run, ) - fnames = args.files + fnames = [str(Path(fn).resolve()) for fn in args.files] if len(args.files) > 1: good = True for fname in args.files: @@ -422,11 +422,11 @@ def main(argv=None, input=None, output=None, force_git_root=None): if len(args.files) == 1: if Path(args.files[0]).is_dir(): if args.git: - git_dname = args.files[0] + git_dname = str(Path(args.files[0]).resolve()) fnames = [] else: io.tool_error(f"{args.files[0]} is a directory, but --no-git selected.") - return -1 + return 1 # We can't know the git repo for sure until after parsing the args. # If we guessed wrong, reparse because that changes things like @@ -506,7 +506,7 @@ def main(argv=None, input=None, output=None, force_git_root=None): ) except ValueError as err: io.tool_error(str(err)) - return + return 1 if args.show_repo_map: repo_map = coder.get_repo_map() diff --git a/tests/test_main.py b/tests/test_main.py index 6bda03c7f..e23268c4c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -44,6 +44,33 @@ class TestMain(TestCase): main(["--yes", "foo.txt"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) + @patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message") + def test_main_with_empty_git_dir_new_files(self, _): + make_repo() + main(["--yes", "foo.txt", "bar.txt"], input=DummyInput(), output=DummyOutput()) + self.assertTrue(os.path.exists("foo.txt")) + self.assertTrue(os.path.exists("bar.txt")) + + def test_main_with_dname_and_fname(self): + subdir = Path("subdir") + subdir.mkdir() + make_repo(str(subdir)) + res = main(["subdir", "foo.txt"], input=DummyInput(), output=DummyOutput()) + self.assertNotEqual(res, None) + + @patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message") + def test_main_with_subdir_repo_fnames(self, _): + subdir = Path("subdir") + subdir.mkdir() + make_repo(str(subdir)) + main( + ["--yes", str(subdir / "foo.txt"), str(subdir / "bar.txt")], + input=DummyInput(), + output=DummyOutput(), + ) + self.assertTrue((subdir / "foo.txt").exists()) + self.assertTrue((subdir / "bar.txt").exists()) + def test_main_with_git_config_yml(self): make_repo() diff --git a/tests/utils.py b/tests/utils.py index 8ece2b4a4..32a2deaf5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -42,7 +42,11 @@ class GitTemporaryDirectory(ChdirTemporaryDirectory): return res -def make_repo(): - repo = git.Repo.init() +def make_repo(path=None): + if not path: + path = "." + repo = git.Repo.init(path) repo.config_writer().set_value("user", "name", "Test User").release() repo.config_writer().set_value("user", "email", "testuser@example.com").release() + + return repo From 5bb3935f7e1d78f4854fd8e3022cf67ce906a232 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Tue, 25 Jul 2023 09:19:01 -0300 Subject: [PATCH 40/40] ssh --- aider/main.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/aider/main.py b/aider/main.py index 97efe8bd5..cceb8fae0 100644 --- a/aider/main.py +++ b/aider/main.py @@ -12,7 +12,7 @@ from aider.io import InputOutput from aider.repo import GitRepo from aider.versioncheck import check_version -from .dump import dump # noqa: F402 +from .dump import dump # noqa: F401 def get_git_root(): @@ -27,15 +27,11 @@ def get_git_root(): def guessed_wrong_repo(io, git_root, fnames, git_dname): """After we parse the args, we can determine the real repo. Did we guess wrong?""" - dump(git_root) - try: check_repo = Path(GitRepo(io, fnames, git_dname).root).resolve() except FileNotFoundError: return - dump(check_repo) - # we had no guess, rely on the "true" repo result if not git_root: return str(check_repo) @@ -44,10 +40,6 @@ def guessed_wrong_repo(io, git_root, fnames, git_dname): if check_repo == git_root: return - print("guessed the wrong repo") - dump(check_repo) - dump(git_root) - return str(check_repo) @@ -434,7 +426,6 @@ def main(argv=None, input=None, output=None, force_git_root=None): if args.git and not force_git_root: right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname) if right_repo_root: - print("guessed wrong") return main(argv, input, output, right_repo_root) io.tool_output(f"Aider v{__version__}")