From 69d53c9783fb2f50ff2321be0c933e0bfd7c5d64 Mon Sep 17 00:00:00 2001 From: Luke Rissacher Date: Mon, 3 Mar 2025 08:48:54 -0800 Subject: [PATCH] Added DirectoryRepo as a GitRepo substitute, so /ls, /map etc. can be used on a dir with other version control systems (--no-git). --- aider/coders/base_coder.py | 8 +-- aider/commands.py | 10 ++-- aider/main.py | 16 +++--- aider/repo_directory.py | 115 +++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 aider/repo_directory.py diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index d377979be..13bd3b49c 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -815,7 +815,7 @@ class Coder: self.shell_commands = [] self.message_cost = 0 - if self.repo: + if isinstance(self.repo, GitRepo): self.commit_before_message.append(self.repo.get_head_commit_sha()) def run(self, with_message=None, preproc=True): @@ -1935,7 +1935,7 @@ class Coder: return all_files - inchat_files - read_only_files def check_for_dirty_commit(self, path): - if not self.repo: + if not isinstance(self.repo, GitRepo): return if not self.dirty_commits: return @@ -2135,7 +2135,7 @@ class Coder: return context def auto_commit(self, edited, context=None): - if not self.repo or not self.auto_commits or self.dry_run: + if not isinstance(self.repo, GitRepo) or not self.auto_commits or self.dry_run: return if not context: @@ -2175,7 +2175,7 @@ class Coder: return if not self.dirty_commits: return - if not self.repo: + if not isinstance(self.repo, GitRepo): return self.repo.commit(fnames=self.need_commit_before_edits) diff --git a/aider/commands.py b/aider/commands.py index b6ece0373..9d53a506b 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -18,7 +18,7 @@ from aider.editor import pipe_editor from aider.format_settings import format_settings from aider.help import Help, install_help_extra from aider.llm import litellm -from aider.repo import ANY_GIT_ERROR +from aider.repo import ANY_GIT_ERROR, GitRepo from aider.run_cmd import run_cmd from aider.scrape import Scraper, install_playwright from aider.utils import is_image_file @@ -282,7 +282,7 @@ class Commands: self.io.tool_error(f"Unable to complete commit: {err}") def raw_cmd_commit(self, args=None): - if not self.coder.repo: + if not isinstance(self.coder.repo, GitRepo): self.io.tool_error("No git repository found.") return @@ -296,7 +296,7 @@ class Commands: def cmd_lint(self, args="", fnames=None): "Lint and fix in-chat files or all dirty files if none in chat" - if not self.coder.repo: + if not isinstance(self.coder.repo, GitRepo): self.io.tool_error("No git repository found.") return @@ -483,7 +483,7 @@ class Commands: self.io.tool_error(f"Unable to complete undo: {err}") def raw_cmd_undo(self, args): - if not self.coder.repo: + if not isinstance(self.coder.repo, GitRepo): self.io.tool_error("No git repository found.") return @@ -585,7 +585,7 @@ class Commands: self.io.tool_error(f"Unable to complete diff: {err}") def raw_cmd_diff(self, args=""): - if not self.coder.repo: + if not isinstance(self.coder.repo, GitRepo): self.io.tool_error("No git repository found.") return diff --git a/aider/main.py b/aider/main.py index 76c9da478..972e5228f 100644 --- a/aider/main.py +++ b/aider/main.py @@ -31,6 +31,7 @@ from aider.io import InputOutput from aider.llm import litellm # noqa: F401; properly init litellm on launch from aider.models import ModelSettings from aider.repo import ANY_GIT_ERROR, GitRepo +from aider.repo_directory import DirectoryRepo from aider.report import report_uncaught_exceptions from aider.versioncheck import check_version, install_from_main_branch, install_upgrade from aider.watch import FileWatcher @@ -686,13 +687,8 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F git_dname = None if len(all_files) == 1: if Path(all_files[0]).is_dir(): - if args.git: - git_dname = str(Path(all_files[0]).resolve()) - fnames = [] - else: - io.tool_error(f"{all_files[0]} is a directory, but --no-git selected.") - analytics.event("exit", reason="Directory with --no-git") - return 1 + git_dname = str(Path(all_files[0]).resolve()) + fnames = [] # We can't know the git repo for sure until after parsing the args. # If we guessed wrong, reparse because that changes things like @@ -861,6 +857,12 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F else: analytics.event("no-repo") + if not repo: + repo = DirectoryRepo( + git_dname, + args.aiderignore, + ) + commands = Commands( io, None, diff --git a/aider/repo_directory.py b/aider/repo_directory.py new file mode 100644 index 000000000..33c2c6363 --- /dev/null +++ b/aider/repo_directory.py @@ -0,0 +1,115 @@ +import os +import time +import pathspec +from pathlib import Path, PurePosixPath +from aider import utils + +class DirectoryRepo: + """ + A substitute for GitRepo; lets Aider run mostly full-featured without a git repo + (for instance, with another version-control tool like fossil). + """ + aider_ignore_file = None + aider_ignore_spec = None + aider_ignore_ts = 0 + aider_ignore_last_check = 0 + + def __init__( + self, + git_dname, + aider_ignore_file=None, + ): + self.normalized_path_cache = {} + self.ignore_file_cache = {} + self.root = utils.safe_abs_path(Path(git_dname or ".").resolve()) + if aider_ignore_file: + self.aider_ignore_file = Path(aider_ignore_file) + + def commit(self, fnames=None, context=None, message=None, aider_edits=False): + return + + def get_rel_repo_dir(self): + return "non-git directory" + + def get_tracked_files(self): + files = [] + for dirpath, _, filenames in os.walk(self.root): + for filename in filenames: + file_path = self.normalize_path(os.path.join(dirpath, filename)) + if self.ignored_file(file_path): + continue + files.append(file_path) + return files + + def normalize_path(self, path): + orig_path = path + res = self.normalized_path_cache.get(orig_path) + if res: + return res + + path = str(Path(PurePosixPath((Path(self.root) / path).relative_to(self.root)))) + self.normalized_path_cache[orig_path] = path + return path + + def refresh_aider_ignore(self): + if not self.aider_ignore_file: + return + + current_time = time.time() + if current_time - self.aider_ignore_last_check < 1: + return + + self.aider_ignore_last_check = current_time + + if not self.aider_ignore_file.is_file(): + return + + mtime = self.aider_ignore_file.stat().st_mtime + if mtime != self.aider_ignore_ts: + self.aider_ignore_ts = mtime + self.ignore_file_cache = {} + lines = self.aider_ignore_file.read_text().splitlines() + self.aider_ignore_spec = pathspec.PathSpec.from_lines( + pathspec.patterns.GitWildMatchPattern, + lines, + ) + + def git_ignored_file(self, path): + return False + + def ignored_file(self, fname): + self.refresh_aider_ignore() + + if fname in self.ignore_file_cache: + return self.ignore_file_cache[fname] + + is_ignored = self.ignored_file_raw(fname) + self.ignore_file_cache[fname] = is_ignored + return is_ignored + + def ignored_file_raw(self, fname): + try: + fname = self.normalize_path(fname) + except ValueError: + return True + + if '.aider' in str(fname): + return True + + if not self.aider_ignore_file or not self.aider_ignore_file.is_file(): + return False + + return self.aider_ignore_spec.match_file(fname) + + def path_in_repo(self, path): + if not path: + return + + tracked_files = set(self.get_tracked_files()) + return self.normalize_path(path) in tracked_files + + def get_dirty_files(self): + return [] + + def is_dirty(self, path=None): + return False