mirror of
https://github.com/Aider-AI/aider.git
synced 2025-05-24 14:25:00 +00:00
Merge branch 'main' into sitter-map
This commit is contained in:
commit
2c98ea4448
25 changed files with 611 additions and 61 deletions
|
@ -7,3 +7,4 @@ OLD*
|
|||
.env
|
||||
.venv
|
||||
.aider.*
|
||||
build
|
||||
|
|
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
|
@ -1,6 +1,7 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
@ -29,3 +30,19 @@ jobs:
|
|||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/aider:${{ github.ref_name }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/aider:latest
|
||||
|
|
28
HISTORY.md
28
HISTORY.md
|
@ -1,5 +1,33 @@
|
|||
# Release history
|
||||
|
||||
### Next release
|
||||
|
||||
- Added support for `.aiderignore` file, which instructs aider to ignore parts of the git repo.
|
||||
- `/run` and `/git` now accept full shell commands, like: `/run (cd subdir; ls)`
|
||||
- New `--commit` cmd line arg, which just commits all pending changes with a sensible commit message geneated by gpt-3.5.
|
||||
- Added universal ctags to the [aider docker image](https://aider.chat/docs/docker.html)
|
||||
|
||||
### v0.14.2
|
||||
|
||||
- Easily [run aider from a docker image](https://aider.chat/docs/docker.html)
|
||||
- Fixed bug with chat history summarization.
|
||||
- Fixed bug if `soundfile` package not available.
|
||||
|
||||
### v0.14.1
|
||||
|
||||
- /add and /drop handle absolute filenames and quoted filenames
|
||||
- /add checks to be sure files are within the git repo (or root)
|
||||
- If needed, warn users that in-chat file paths are all relative to the git repo
|
||||
- Fixed /add bug in when aider launched in repo subdir
|
||||
- Show models supported by api/key if requested model isn't available
|
||||
|
||||
### v0.14.0
|
||||
|
||||
- [Support for Claude2 and other LLMs via OpenRouter](https://aider.chat/docs/faq.html#accessing-other-llms-with-openrouter) by @joshuavial
|
||||
- Documentation for [running the aider benchmarking suite](https://github.com/paul-gauthier/aider/tree/main/benchmark)
|
||||
- Aider now requires Python >= 3.9
|
||||
|
||||
|
||||
### v0.13.0
|
||||
|
||||
- [Only git commit dirty files that GPT tries to edit](https://aider.chat/docs/faq.html#how-did-v0130-change-git-usage)
|
||||
|
|
|
@ -134,7 +134,6 @@ Aider has some ability to help GPT figure out which files to edit all by itself,
|
|||
* Use Meta-ENTER (Esc+ENTER in some environments) to enter multiline chat messages. Or enter `{` alone on the first line to start a multiline message and `}` alone on the last line to end it.
|
||||
* If your code is throwing an error, share the error output with GPT using `/run` or by pasting it into the chat. Let GPT figure out and fix the bug.
|
||||
* GPT knows about a lot of standard tools and libraries, but may get some of the fine details wrong about APIs and function arguments. You can paste doc snippets into the chat to resolve these issues.
|
||||
* [Aider will notice if you launch it on a git repo with uncommitted changes and offer to commit them before proceeding](https://aider.chat/docs/faq.html#how-does-aider-use-git).
|
||||
* GPT can only see the content of the files you specifically "add to the chat". Aider also sends GPT-4 a [map of your entire git repo](https://aider.chat/docs/ctags.html). So GPT may ask to see additional files if it feels that's needed for your requests.
|
||||
* I also shared some general [GPT coding tips on Hacker News](https://news.ycombinator.com/item?id=36211879).
|
||||
|
||||
|
@ -170,4 +169,4 @@ For more information, see the [FAQ](https://aider.chat/docs/faq.html).
|
|||
* *This project is stellar.* -- [funkytaco](https://github.com/paul-gauthier/aider/issues/112#issuecomment-1637429008)
|
||||
* *Amazing project, definitely the best AI coding assistant I've used.* -- [joshuavial](https://github.com/paul-gauthier/aider/issues/84)
|
||||
* *I am an aider addict. I'm getting so much more work done, but in less time.* -- [dandandan](https://discord.com/channels/1131200896827654144/1131200896827654149/1135913253483069470)
|
||||
* *Best agent for actual dev work in existing codebases.* -- [Nick Dobos](https://twitter.com/NickADobos/status/1690408967963652097?s=20)
|
||||
* *Best agent for actual dev work in existing codebases.* -- [Nick Dobos](https://twitter.com/NickADobos/status/1690408967963652097?s=20)
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "0.13.1-dev"
|
||||
__version__ = "0.14.3-dev"
|
||||
|
|
|
@ -55,6 +55,7 @@ class Coder:
|
|||
main_model,
|
||||
edit_format,
|
||||
io,
|
||||
skip_model_availabily_check=False,
|
||||
**kwargs,
|
||||
):
|
||||
from . import EditBlockCoder, WholeFileCoder
|
||||
|
@ -62,8 +63,8 @@ class Coder:
|
|||
if not main_model:
|
||||
main_model = models.GPT35_16k
|
||||
|
||||
if not main_model.always_available:
|
||||
if not check_model_availability(main_model):
|
||||
if not skip_model_availabily_check and not main_model.always_available:
|
||||
if not check_model_availability(io, main_model):
|
||||
if main_model != models.GPT4:
|
||||
io.tool_error(
|
||||
f"API key does not support {main_model.name}, falling back to"
|
||||
|
@ -99,6 +100,7 @@ class Coder:
|
|||
stream=True,
|
||||
use_git=True,
|
||||
voice_language=None,
|
||||
aider_ignore_file=None,
|
||||
):
|
||||
if not fnames:
|
||||
fnames = []
|
||||
|
@ -153,7 +155,7 @@ class Coder:
|
|||
|
||||
if use_git:
|
||||
try:
|
||||
self.repo = GitRepo(self.io, fnames, git_dname)
|
||||
self.repo = GitRepo(self.io, fnames, git_dname, aider_ignore_file)
|
||||
self.root = self.repo.root
|
||||
except FileNotFoundError:
|
||||
self.repo = None
|
||||
|
@ -185,7 +187,7 @@ class Coder:
|
|||
|
||||
self.summarizer = ChatSummary(models.Model.weak_model())
|
||||
self.summarizer_thread = None
|
||||
self.summarized_done_messages = None
|
||||
self.summarized_done_messages = []
|
||||
|
||||
# validate the functions jsonschema
|
||||
if self.functions:
|
||||
|
@ -352,7 +354,11 @@ class Coder:
|
|||
self.summarizer_thread.start()
|
||||
|
||||
def summarize_worker(self):
|
||||
self.summarized_done_messages = self.summarizer.summarize(self.done_messages)
|
||||
try:
|
||||
self.summarized_done_messages = self.summarizer.summarize(self.done_messages)
|
||||
except ValueError as err:
|
||||
self.io.tool_error(err.args[0])
|
||||
|
||||
if self.verbose:
|
||||
self.io.tool_output("Finished summarizing chat history.")
|
||||
|
||||
|
@ -364,7 +370,7 @@ class Coder:
|
|||
self.summarizer_thread = None
|
||||
|
||||
self.done_messages = self.summarized_done_messages
|
||||
self.summarized_done_messages = None
|
||||
self.summarized_done_messages = []
|
||||
|
||||
def move_back_cur_messages(self, message):
|
||||
self.done_messages += self.cur_messages
|
||||
|
@ -595,15 +601,17 @@ class Coder:
|
|||
self.io.tool_error(show_content_err)
|
||||
raise Exception("No data found in openai response!")
|
||||
|
||||
prompt_tokens = completion.usage.prompt_tokens
|
||||
completion_tokens = completion.usage.completion_tokens
|
||||
tokens = None
|
||||
if hasattr(completion, "usage"):
|
||||
prompt_tokens = completion.usage.prompt_tokens
|
||||
completion_tokens = completion.usage.completion_tokens
|
||||
|
||||
tokens = f"{prompt_tokens} prompt tokens, {completion_tokens} completion tokens"
|
||||
if self.main_model.prompt_price:
|
||||
cost = prompt_tokens * self.main_model.prompt_price / 1000
|
||||
cost += completion_tokens * self.main_model.completion_price / 1000
|
||||
tokens += f", ${cost:.6f} cost"
|
||||
self.total_cost += cost
|
||||
tokens = f"{prompt_tokens} prompt tokens, {completion_tokens} completion tokens"
|
||||
if self.main_model.prompt_price:
|
||||
cost = prompt_tokens * self.main_model.prompt_price / 1000
|
||||
cost += completion_tokens * self.main_model.completion_price / 1000
|
||||
tokens += f", ${cost:.6f} cost"
|
||||
self.total_cost += cost
|
||||
|
||||
show_resp = self.render_incremental_response(True)
|
||||
if self.pretty:
|
||||
|
@ -614,7 +622,9 @@ class Coder:
|
|||
show_resp = Text(show_resp or "<no response>")
|
||||
|
||||
self.io.console.print(show_resp)
|
||||
self.io.tool_output(tokens)
|
||||
|
||||
if tokens is not None:
|
||||
self.io.tool_output(tokens)
|
||||
|
||||
def show_send_output_stream(self, completion):
|
||||
live = None
|
||||
|
@ -895,7 +905,12 @@ class Coder:
|
|||
return True
|
||||
|
||||
|
||||
def check_model_availability(main_model):
|
||||
def check_model_availability(io, main_model):
|
||||
available_models = openai.Model.list()
|
||||
model_ids = [model.id for model in available_models["data"]]
|
||||
return main_model.name in model_ids
|
||||
if main_model.name in model_ids:
|
||||
return True
|
||||
|
||||
available_models = ", ".join(model_ids)
|
||||
io.tool_error(f"API key supports: {available_models}")
|
||||
return False
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
import shlex
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
@ -209,6 +209,10 @@ class Commands:
|
|||
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.")
|
||||
self.io.tool_error(
|
||||
"You could try `/git reset --hard HEAD^` but be aware that this is a destructive"
|
||||
" command!"
|
||||
)
|
||||
return
|
||||
self.coder.repo.repo.git.reset("--hard", "HEAD~1")
|
||||
self.io.tool_output(
|
||||
|
@ -228,6 +232,7 @@ class Commands:
|
|||
|
||||
if not self.coder.last_aider_commit_hash:
|
||||
self.io.tool_error("No previous aider commit found.")
|
||||
self.io.tool_error("You could try `/git diff` or `/git diff HEAD^`.")
|
||||
return
|
||||
|
||||
commits = f"{self.coder.last_aider_commit_hash}~1"
|
||||
|
@ -272,29 +277,37 @@ class Commands:
|
|||
git_files = self.coder.repo.get_tracked_files() if self.coder.repo else []
|
||||
|
||||
all_matched_files = set()
|
||||
for word in args.split():
|
||||
|
||||
filenames = parse_quoted_filenames(args)
|
||||
for word in filenames:
|
||||
if Path(word).is_absolute():
|
||||
fname = Path(word)
|
||||
else:
|
||||
fname = Path(self.coder.root) / word
|
||||
|
||||
if fname.exists() and fname.is_file():
|
||||
all_matched_files.add(str(fname))
|
||||
continue
|
||||
# an existing dir will fall through and get recursed by glob
|
||||
|
||||
matched_files = self.glob_filtered_to_repo(word)
|
||||
if matched_files:
|
||||
all_matched_files.update(matched_files)
|
||||
continue
|
||||
|
||||
if not matched_files:
|
||||
if any(char in word for char in "*?[]"):
|
||||
self.io.tool_error(f"No files to add matching pattern: {word}")
|
||||
else:
|
||||
if Path(word).exists():
|
||||
if Path(word).is_file():
|
||||
matched_files = [word]
|
||||
else:
|
||||
self.io.tool_error(f"Unable to add: {word}")
|
||||
elif self.io.confirm_ask(
|
||||
f"No files matched '{word}'. Do you want to create the file?"
|
||||
):
|
||||
(Path(self.coder.root) / word).touch()
|
||||
matched_files = [word]
|
||||
|
||||
all_matched_files.update(matched_files)
|
||||
if self.io.confirm_ask(f"No files matched '{word}'. Do you want to create {fname}?"):
|
||||
fname.touch()
|
||||
all_matched_files.add(str(fname))
|
||||
|
||||
for matched_file in all_matched_files:
|
||||
abs_file_path = self.coder.abs_root_path(matched_file)
|
||||
|
||||
if not abs_file_path.startswith(self.coder.root):
|
||||
self.io.tool_error(
|
||||
f"Can not add {abs_file_path}, which is not within {self.coder.root}"
|
||||
)
|
||||
continue
|
||||
|
||||
if self.coder.repo and matched_file not in git_files:
|
||||
self.coder.repo.repo.git.add(abs_file_path)
|
||||
git_added.append(matched_file)
|
||||
|
@ -339,7 +352,8 @@ class Commands:
|
|||
self.io.tool_output("Dropping all files from the chat session.")
|
||||
self.coder.abs_fnames = set()
|
||||
|
||||
for word in args.split():
|
||||
filenames = parse_quoted_filenames(args)
|
||||
for word in filenames:
|
||||
matched_files = self.glob_filtered_to_repo(word)
|
||||
|
||||
if not matched_files:
|
||||
|
@ -355,10 +369,15 @@ class Commands:
|
|||
"Run a git command"
|
||||
combined_output = None
|
||||
try:
|
||||
parsed_args = shlex.split("git " + args)
|
||||
args = "git " + args
|
||||
env = dict(GIT_EDITOR="true", **subprocess.os.environ)
|
||||
result = subprocess.run(
|
||||
parsed_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
env=env,
|
||||
shell=True,
|
||||
)
|
||||
combined_output = result.stdout
|
||||
except Exception as e:
|
||||
|
@ -373,9 +392,8 @@ class Commands:
|
|||
"Run a shell command and optionally add the output to the chat"
|
||||
combined_output = None
|
||||
try:
|
||||
parsed_args = shlex.split(args)
|
||||
result = subprocess.run(
|
||||
parsed_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
||||
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, shell=True
|
||||
)
|
||||
combined_output = result.stdout
|
||||
except Exception as e:
|
||||
|
@ -447,7 +465,9 @@ class Commands:
|
|||
try:
|
||||
self.voice = voice.Voice()
|
||||
except voice.SoundDeviceError:
|
||||
self.io.tool_error("Unable to import `sounddevice`, is portaudio installed?")
|
||||
self.io.tool_error(
|
||||
"Unable to import `sounddevice` and/or `soundfile`, is portaudio installed?"
|
||||
)
|
||||
return
|
||||
|
||||
history_iter = self.io.get_input_history()
|
||||
|
@ -487,3 +507,9 @@ def expand_subdir(file_path):
|
|||
for file in file_path.rglob("*"):
|
||||
if file.is_file():
|
||||
yield str(file)
|
||||
|
||||
|
||||
def parse_quoted_filenames(args):
|
||||
filenames = re.findall(r"\"(.+?)\"|(\S+)", args)
|
||||
filenames = [name for sublist in filenames for name in sublist if name]
|
||||
return filenames
|
||||
|
|
|
@ -85,6 +85,8 @@ class ChatSummary:
|
|||
]
|
||||
|
||||
summary = simple_send_with_retries(self.model.name, messages)
|
||||
if summary is None:
|
||||
raise ValueError(f"summarizer unexpectedly failed for {self.model.name}")
|
||||
summary = prompts.summary_prefix + summary
|
||||
|
||||
return [dict(role="user", content=summary)]
|
||||
|
|
|
@ -60,10 +60,10 @@ def setup_git(git_root, io):
|
|||
with repo.config_writer() as git_config:
|
||||
if not global_git_config.has_option("user", "name"):
|
||||
git_config.set_value("user", "name", "Your Name")
|
||||
io.tool_error('Update git name with: git config --global user.name "Your Name"')
|
||||
io.tool_error('Update git name with: git config user.name "Your Name"')
|
||||
if not global_git_config.has_option("user", "email"):
|
||||
git_config.set_value("user", "email", "you@example.com")
|
||||
io.tool_error('Update git email with: git config --global user.email "you@example.com"')
|
||||
io.tool_error('Update git email with: git config user.email "you@example.com"')
|
||||
|
||||
io.tool_output("Git repository created in the current working directory.")
|
||||
|
||||
|
@ -74,6 +74,13 @@ def check_gitignore(git_root, io, ask=True):
|
|||
if not git_root:
|
||||
return
|
||||
|
||||
try:
|
||||
repo = git.Repo(git_root)
|
||||
if repo.ignored(".aider"):
|
||||
return
|
||||
except git.exc.InvalidGitRepositoryError:
|
||||
pass
|
||||
|
||||
pat = ".aider*"
|
||||
|
||||
gitignore_file = Path(git_root) / ".gitignore"
|
||||
|
@ -142,6 +149,12 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
default=models.GPT4.name,
|
||||
help=f"Specify the model to use for the main chat (default: {models.GPT4.name})",
|
||||
)
|
||||
core_group.add_argument(
|
||||
"--skip-model-availability-check",
|
||||
metavar="SKIP_MODEL_AVAILABILITY_CHECK",
|
||||
default=False,
|
||||
help="Override to skip model availability check (default: False)",
|
||||
)
|
||||
core_group.add_argument(
|
||||
"-3",
|
||||
action="store_const",
|
||||
|
@ -286,6 +299,21 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
default=True,
|
||||
help="Enable/disable looking for a git repo (default: True)",
|
||||
)
|
||||
git_group.add_argument(
|
||||
"--gitignore",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="Enable/disable adding .aider* to .gitignore (default: True)",
|
||||
)
|
||||
default_aiderignore_file = (
|
||||
os.path.join(git_root, ".aiderignore") if git_root else ".aiderignore"
|
||||
)
|
||||
git_group.add_argument(
|
||||
"--aiderignore",
|
||||
metavar="AIDERIGNORE",
|
||||
default=default_aiderignore_file,
|
||||
help="Specify the aider ignore file (default: .aiderignore in git root)",
|
||||
)
|
||||
git_group.add_argument(
|
||||
"--auto-commits",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
|
@ -302,7 +330,13 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
"--dry-run",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Enable/disable performing a dry run without modifying files (default: False)",
|
||||
help="Perform a dry run without modifying files (default: False)",
|
||||
)
|
||||
git_group.add_argument(
|
||||
"--commit",
|
||||
action="store_true",
|
||||
help="Commit all pending changes with a suitable commit message, then exit",
|
||||
default=False,
|
||||
)
|
||||
|
||||
##########
|
||||
|
@ -423,7 +457,8 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
|
||||
if args.git:
|
||||
git_root = setup_git(git_root, io)
|
||||
check_gitignore(git_root, io)
|
||||
if args.gitignore:
|
||||
check_gitignore(git_root, io)
|
||||
|
||||
def scrub_sensitive_info(text):
|
||||
# Replace sensitive information with placeholder
|
||||
|
@ -465,6 +500,7 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
main_model,
|
||||
args.edit_format,
|
||||
io,
|
||||
args.skip_model_availability_check,
|
||||
##
|
||||
fnames=fnames,
|
||||
git_dname=git_dname,
|
||||
|
@ -480,11 +516,16 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
stream=args.stream,
|
||||
use_git=args.git,
|
||||
voice_language=args.voice_language,
|
||||
aider_ignore_file=args.aiderignore,
|
||||
)
|
||||
except ValueError as err:
|
||||
io.tool_error(str(err))
|
||||
return 1
|
||||
|
||||
if args.commit:
|
||||
coder.commands.cmd_commit("")
|
||||
return
|
||||
|
||||
if args.show_repo_map:
|
||||
repo_map = coder.get_repo_map()
|
||||
if repo_map:
|
||||
|
@ -501,6 +542,15 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
|
||||
io.tool_output("Use /help to see in-chat commands, run with --help to see cmd line args")
|
||||
|
||||
if git_root and Path.cwd().resolve() != Path(git_root).resolve():
|
||||
io.tool_error(
|
||||
"Note: in-chat filenames are always relative to the git working dir, not the current"
|
||||
" working dir."
|
||||
)
|
||||
|
||||
io.tool_error(f"Cur working dir: {Path.cwd()}")
|
||||
io.tool_error(f"Git working dir: {git_root}")
|
||||
|
||||
if args.message:
|
||||
io.tool_output()
|
||||
coder.run(with_message=args.message)
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
from .model import Model
|
||||
from .openai import OpenAIModel
|
||||
from .openrouter import OpenRouterModel
|
||||
|
||||
GPT4 = Model.create("gpt-4")
|
||||
GPT35 = Model.create("gpt-3.5-turbo")
|
||||
GPT35_16k = Model.create("gpt-3.5-turbo-16k")
|
||||
|
||||
__all__ = [
|
||||
OpenAIModel,
|
||||
OpenRouterModel,
|
||||
GPT4,
|
||||
GPT35,
|
||||
GPT35_16k,
|
||||
]
|
||||
|
|
|
@ -2,6 +2,7 @@ import os
|
|||
from pathlib import Path, PurePosixPath
|
||||
|
||||
import git
|
||||
import pathspec
|
||||
|
||||
from aider import models, prompts, utils
|
||||
from aider.sendchat import simple_send_with_retries
|
||||
|
@ -11,8 +12,11 @@ from .dump import dump # noqa: F401
|
|||
|
||||
class GitRepo:
|
||||
repo = None
|
||||
aider_ignore_file = None
|
||||
aider_ignore_spec = None
|
||||
aider_ignore_ts = 0
|
||||
|
||||
def __init__(self, io, fnames, git_dname):
|
||||
def __init__(self, io, fnames, git_dname, aider_ignore_file=None):
|
||||
self.io = io
|
||||
|
||||
if git_dname:
|
||||
|
@ -49,14 +53,20 @@ class GitRepo:
|
|||
self.repo = git.Repo(repo_paths.pop(), odbt=git.GitDB)
|
||||
self.root = utils.safe_abs_path(self.repo.working_tree_dir)
|
||||
|
||||
if aider_ignore_file:
|
||||
self.aider_ignore_file = Path(aider_ignore_file)
|
||||
|
||||
def commit(self, fnames=None, context=None, prefix=None, message=None):
|
||||
if not fnames and not self.repo.is_dirty():
|
||||
return
|
||||
|
||||
diffs = self.get_diffs(fnames)
|
||||
if not diffs:
|
||||
return
|
||||
|
||||
if message:
|
||||
commit_message = message
|
||||
else:
|
||||
diffs = self.get_diffs(fnames)
|
||||
commit_message = self.get_commit_message(diffs, context)
|
||||
|
||||
if not commit_message:
|
||||
|
@ -190,7 +200,22 @@ class GitRepo:
|
|||
for path in files
|
||||
)
|
||||
|
||||
return res
|
||||
return self.filter_ignored_files(res)
|
||||
|
||||
def filter_ignored_files(self, fnames):
|
||||
if not self.aider_ignore_file or not self.aider_ignore_file.is_file():
|
||||
return fnames
|
||||
|
||||
mtime = self.aider_ignore_file.stat().st_mtime
|
||||
if mtime != self.aider_ignore_ts:
|
||||
self.aider_ignore_ts = mtime
|
||||
lines = self.aider_ignore_file.read_text().splitlines()
|
||||
self.aider_ignore_spec = pathspec.PathSpec.from_lines(
|
||||
pathspec.patterns.GitWildMatchPattern,
|
||||
lines,
|
||||
)
|
||||
|
||||
return [fname for fname in fnames if not self.aider_ignore_spec.match_file(fname)]
|
||||
|
||||
def path_in_repo(self, path):
|
||||
if not self.repo:
|
||||
|
|
|
@ -83,6 +83,8 @@ class RepoMap:
|
|||
|
||||
cache_missing = False
|
||||
|
||||
warned_files = set()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
map_tokens=1024,
|
||||
|
@ -235,7 +237,10 @@ class RepoMap:
|
|||
|
||||
for fname in fnames:
|
||||
if not Path(fname).is_file():
|
||||
self.io.tool_error(f"Repo-map can't include {fname}")
|
||||
if fname not in self.warned_files:
|
||||
self.io.tool_error(f"Repo-map can't include {fname}")
|
||||
|
||||
self.warned_files.add(fname)
|
||||
continue
|
||||
|
||||
# dump(fname)
|
||||
|
|
|
@ -5,7 +5,12 @@ import time
|
|||
|
||||
import numpy as np
|
||||
import openai
|
||||
import soundfile as sf
|
||||
|
||||
try:
|
||||
import soundfile as sf
|
||||
except (OSError, ModuleNotFoundError):
|
||||
sf = None
|
||||
|
||||
from prompt_toolkit.shortcuts import prompt
|
||||
|
||||
from .dump import dump # noqa: F401
|
||||
|
@ -23,12 +28,14 @@ class Voice:
|
|||
threshold = 0.15
|
||||
|
||||
def __init__(self):
|
||||
if sf is None:
|
||||
raise SoundDeviceError
|
||||
try:
|
||||
print("Initializing sound device...")
|
||||
import sounddevice as sd
|
||||
|
||||
self.sd = sd
|
||||
except OSError:
|
||||
except (OSError, ModuleNotFoundError):
|
||||
raise SoundDeviceError
|
||||
|
||||
def callback(self, indata, frames, time, status):
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
FROM python:3.8-slim
|
||||
FROM python:3.10-slim
|
||||
RUN apt-get update && apt-get install -y less git
|
||||
COPY requirements.txt /aider/requirements.txt
|
||||
RUN pip install lox typer pandas matplotlib imgcat aider-chat
|
||||
RUN pip install --upgrade pip && pip install -r /aider/requirements.txt
|
||||
RUN git config --global --add safe.directory /aider
|
||||
WORKDIR /aider
|
||||
|
||||
|
|
107
benchmark/README.md
Normal file
107
benchmark/README.md
Normal file
|
@ -0,0 +1,107 @@
|
|||
|
||||
# Aider code editing benchmark harness
|
||||
|
||||
Aider uses a "code editing" benchmark to quantitatively measure how well it works
|
||||
with the GPT-3.5 and GPT-4 models.
|
||||
This directory holds the harness and tools needed to run the benchmarking suite.
|
||||
|
||||
## Background
|
||||
|
||||
The benchmark is based on the [Exercism
|
||||
python](https://github.com/exercism/python) coding exercises.
|
||||
This
|
||||
benchmark evaluates how effectively aider and GPT can translate a
|
||||
natural language coding request into executable code saved into
|
||||
files that pass unit tests.
|
||||
It provides an end-to-end evaluation of not just
|
||||
GPT's coding ability, but also its capacity to *edit existing code*
|
||||
and *format those code edits* so that aider can save the
|
||||
edits to the local source files.
|
||||
|
||||
See [this writeup for a longer discussion about the benchmark and how to interpret the results](https://aider.chat/docs/benchmarks.html).
|
||||
|
||||
The benchmark is intended to be run *inside a docker container*.
|
||||
This is because the benchmarking harness will be
|
||||
taking code written by an LLM
|
||||
and executing it without any human review or supervision!
|
||||
The LLM could generate dangerous python that harms your system, like this: `import os; os.system("sudo rm -rf /")`.
|
||||
Running inside a docker container helps limit the damage that could be done.
|
||||
|
||||
## Usage
|
||||
|
||||
There are 3 main tasks involved in benchmarking aider:
|
||||
|
||||
1. Install and setup for benchmarking.
|
||||
|
||||
2. Run the benchmark to measure performance across the 133 exercises.
|
||||
|
||||
3. Generate a summary report of how many of the exercises succeeded or failed.
|
||||
|
||||
### Setup for benchmarking
|
||||
|
||||
First, prepare all the groundwork for running the benchmarks.
|
||||
These steps only need to be done once.
|
||||
|
||||
```
|
||||
# Clone the aider repo
|
||||
git clone git@github.com:paul-gauthier/aider.git
|
||||
|
||||
# Create the scratch dir to hold benchmarking results inside the main aider dir:
|
||||
cd aider
|
||||
mkdir tmp.benchmarks
|
||||
|
||||
# Clone the exercism repo
|
||||
git clone git@github.com:exercism/python.git
|
||||
|
||||
# Copy the practice exercises into the benchmark scratch dir
|
||||
cp -rp python/exercises/practice tmp.benchmarks/practice
|
||||
|
||||
# Build the docker container
|
||||
./benchmark/docker_build.sh
|
||||
```
|
||||
|
||||
### Running the benchmark
|
||||
|
||||
Launch the docker container and run the benchmark inside it:
|
||||
|
||||
```
|
||||
# Launch the docker container
|
||||
./benchmark/docker.sh
|
||||
|
||||
# Inside the container, install aider as a development build.
|
||||
# This way you're running the code that you cloned above, including any local changes.
|
||||
pip install -e .
|
||||
|
||||
# Run the benchmark:
|
||||
./benchmark/benchmark.py a-helpful-name-for-this-run --model gpt-3.5-turbo --edit-format whole --threads 10
|
||||
```
|
||||
|
||||
The above will create a folder `tmp.benchmarks/YYYY-MM-DD-HH-MM-SS--a-helpful-name-for-this-run` with benchmarking results.
|
||||
Run like this, the script will run all 133 exercises in a random order.
|
||||
|
||||
You can run `./benchmark/benchmark.py --help` for a list of all the arguments, but here are the most useful to keep in mind:
|
||||
|
||||
- `--model` is the name of the model, same as you would pass directly to `aider`.
|
||||
- `--edit-format` is the name of the edit format, same as you would pass directly to `aider`. When working with an experimental LLM, I recommend starting with `whole`
|
||||
- `--threads` specifies how many exercises to benchmark in parallel. Start with a single thread if you are working out the kinks on your benchmarking setup or working with a new model, etc. Once you are getting reliable results, you can speed up the process by running with more threads. 10 works well against the OpenAI APIs.
|
||||
- `--num-tests` specifies how many of the 133 tests to run before stopping. This is another way to start gently as you debug your benchmarking setup.
|
||||
- `--keywords` filters the tests to run to only the ones whose name match the supplied argument (similar to `pytest -k xxxx`).
|
||||
|
||||
### Generating a benchmark report
|
||||
|
||||
You can generate stats about any benchmark, including ones which are still running.
|
||||
You don't need to run this inside the docker container, as it is just
|
||||
collecting stats not executing unsafe python.
|
||||
|
||||
```
|
||||
# Generate stats for a specific benchmarking directory
|
||||
./benchmark/benchmark.py --stats tmp.benchmarks/YYYY-MM-DD-HH-MM-SS--a-helpful-name-for-this-run
|
||||
```
|
||||
|
||||
## Limitations, notes
|
||||
|
||||
- If you're experimenting with non-OpenAI models, the benchmarking harness may not provide enough switches/control to specify the integration to such models. You probably need to edit `benchmark.py` to instantiate `Coder()` appropriately. You can just hack this in or add new switches/config.
|
||||
- Benchmarking all 133 exercises against GPT-4 will cost about $10-20.
|
||||
- Benchmarking aider is intended for folks who are actively developing aider or doing experimental work adapting it for use with [new LLM models](https://github.com/paul-gauthier/aider/issues/172).
|
||||
- These scripts are not intended for use by typical aider users.
|
||||
- Some of the tools are written as `bash` scripts, so it will be hard to use them on Windows.
|
|
@ -547,7 +547,7 @@ def run_test(
|
|||
chat_history_file=history_fname,
|
||||
)
|
||||
|
||||
main_model = models.Model(model_name)
|
||||
main_model = models.Model.create(model_name)
|
||||
edit_format = edit_format or main_model.edit_format
|
||||
|
||||
dump(main_model)
|
||||
|
|
9
docker/Dockerfile
Normal file
9
docker/Dockerfile
Normal file
|
@ -0,0 +1,9 @@
|
|||
FROM python:3.10-slim
|
||||
COPY . /aider
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y git universal-ctags libportaudio2 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
pip install --no-cache-dir /aider && \
|
||||
rm -rf /aider
|
||||
WORKDIR /app
|
||||
ENTRYPOINT ["aider"]
|
32
docs/docker.md
Normal file
32
docs/docker.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
|
||||
# Run aider with docker (experimental)
|
||||
|
||||
You can run aider via docker without doing any local installation, like this:
|
||||
|
||||
```
|
||||
docker pull paulgauthier/aider
|
||||
docker run -it --volume $(pwd):/app paulgauthier/aider --openai-api-key $OPENAI_API_KEY [...other aider args...]
|
||||
```
|
||||
|
||||
You should run the above commands from the root of your git repo,
|
||||
since the `--volume` arg maps your current directory into the
|
||||
docker container.
|
||||
Given that, you need to be in the root of your git repo for aider to be able to
|
||||
see the repo and all its files.
|
||||
|
||||
You should be sure your that
|
||||
git repo config contains your user name and email, since the
|
||||
docker container won't have your global git config.
|
||||
Run these commands while in your git repo, before
|
||||
you do the `docker run` command:
|
||||
|
||||
```
|
||||
git config user.email "you@example.com"
|
||||
git config user.name "Your Name"
|
||||
```
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
- When you use the in-chat `/run` command, it will be running shell commands *inside the docker container*. So those commands won't be running in your local environment, which may make it tricky to `/run` tests, etc for your project.
|
||||
- The `/voice` command won't work unless you can figure out how to give the docker container access to your host audio device. The container has libportaudio2 installed, so it should work if you can do that.
|
19
docs/faq.md
19
docs/faq.md
|
@ -8,6 +8,7 @@
|
|||
- [Can I use aider with other LLMs, local LLMs, etc?](#can-i-use-aider-with-other-llms-local-llms-etc)
|
||||
- [Can I change the system prompts that aider uses?](#can-i-change-the-system-prompts-that-aider-uses)
|
||||
- [Can I run aider in Google Colab?](#can-i-run-aider-in-google-colab)
|
||||
- [How can I run aider locally from source code?](#how-can-i-run-aider-locally-from-source-code)
|
||||
|
||||
## How does aider use git?
|
||||
|
||||
|
@ -224,3 +225,21 @@ all the raw information being sent to/from GPT in the conversation.
|
|||
User [imabutahersiddik](https://github.com/imabutahersiddik)
|
||||
has provided this
|
||||
[Colab notebook](https://colab.research.google.com/drive/1J9XynhrCqekPL5PR6olHP6eE--rnnjS9?usp=sharing).
|
||||
|
||||
## How can I run aider locally from source code?
|
||||
|
||||
To run the project locally, follow these steps:
|
||||
|
||||
```
|
||||
# Clone the repository:
|
||||
git clone git@github.com:paul-gauthier/aider.git
|
||||
|
||||
# Navigate to the project directory:
|
||||
cd aider
|
||||
|
||||
# Install the dependencies listed in the `requirements.txt` file:
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run the local version of Aider:
|
||||
python -m aider.main
|
||||
```
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
Speak with GPT about your code! Request new features, test cases or bug fixes using your voice and let GPT do the work of editing the files in your local git repo. As with all of aider's capabilities, you can use voice-to-code with an existing repo or to start a new project.
|
||||
|
||||
Voice support fits quite naturally into aider's AI pair programming chat interface. Now you can fluidly switch between voice and text chat where you ask GPT to edit the code and your own editor when it makes more sense for you to drive.
|
||||
Voice support fits quite naturally into aider's AI pair programming
|
||||
chat interface. Now you can fluidly switch between voice and text chat
|
||||
when you ask GPT to edit your code.
|
||||
|
||||
## How to use voice-to-code
|
||||
|
||||
|
|
0
hello.py
0
hello.py
|
@ -33,3 +33,4 @@ scipy==1.10.1
|
|||
jsonschema==4.17.3
|
||||
sounddevice==0.4.6
|
||||
soundfile==0.12.1
|
||||
pathspec==0.11.2
|
||||
|
|
2
scripts/versionbump.py
Normal file → Executable file
2
scripts/versionbump.py
Normal file → Executable file
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
|
|
|
@ -14,7 +14,7 @@ from aider.coders import Coder
|
|||
from aider.commands import Commands
|
||||
from aider.dump import dump # noqa: F401
|
||||
from aider.io import InputOutput
|
||||
from tests.utils import GitTemporaryDirectory
|
||||
from tests.utils import ChdirTemporaryDirectory, GitTemporaryDirectory, make_repo
|
||||
|
||||
|
||||
class TestCommands(TestCase):
|
||||
|
@ -69,8 +69,8 @@ class TestCommands(TestCase):
|
|||
self.assertNotIn(str(Path("test.txt").resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_no_match(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
# yes=False means we will *not* create the file when it is not found
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(models.GPT35, None, io)
|
||||
|
@ -82,6 +82,23 @@ class TestCommands(TestCase):
|
|||
# Check if no files have been added to the chat session
|
||||
self.assertEqual(len(coder.abs_fnames), 0)
|
||||
|
||||
def test_cmd_add_no_match_but_make_it(self):
|
||||
# yes=True means we *will* create the file when it is not found
|
||||
io = InputOutput(pretty=False, yes=True)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(models.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("[abc].nonexistent")
|
||||
|
||||
# Call the cmd_add method with a non-existent file pattern
|
||||
commands.cmd_add(str(fname))
|
||||
|
||||
# Check if no files have been added to the chat session
|
||||
self.assertEqual(len(coder.abs_fnames), 1)
|
||||
self.assertTrue(fname.exists())
|
||||
|
||||
def test_cmd_add_drop_directory(self):
|
||||
# Initialize the Commands and InputOutput objects
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
|
@ -255,6 +272,25 @@ class TestCommands(TestCase):
|
|||
self.assertNotIn(filenames[1], coder.abs_fnames)
|
||||
self.assertIn(filenames[2], coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_from_subdir_again(self):
|
||||
with GitTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(models.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
Path("side_dir").mkdir()
|
||||
os.chdir("side_dir")
|
||||
|
||||
# add a file that is in the side_dir
|
||||
with open("temp.txt", "w"):
|
||||
pass
|
||||
|
||||
# this was blowing up with GitCommandError, per:
|
||||
# https://github.com/paul-gauthier/aider/issues/201
|
||||
commands.cmd_add("temp.txt")
|
||||
|
||||
def test_cmd_commit(self):
|
||||
with GitTemporaryDirectory():
|
||||
fname = "test.txt"
|
||||
|
@ -276,3 +312,93 @@ class TestCommands(TestCase):
|
|||
commit_message = "Test commit message"
|
||||
commands.cmd_commit(commit_message)
|
||||
self.assertFalse(repo.is_dirty())
|
||||
|
||||
def test_cmd_add_from_outside_root(self):
|
||||
with ChdirTemporaryDirectory() as tmp_dname:
|
||||
root = Path("root")
|
||||
root.mkdir()
|
||||
os.chdir(str(root))
|
||||
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(models.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
outside_file = Path(tmp_dname) / "outside.txt"
|
||||
outside_file.touch()
|
||||
|
||||
# This should not be allowed!
|
||||
# https://github.com/paul-gauthier/aider/issues/178
|
||||
commands.cmd_add("../outside.txt")
|
||||
|
||||
self.assertEqual(len(coder.abs_fnames), 0)
|
||||
|
||||
def test_cmd_add_from_outside_git(self):
|
||||
with ChdirTemporaryDirectory() as tmp_dname:
|
||||
root = Path("root")
|
||||
root.mkdir()
|
||||
os.chdir(str(root))
|
||||
|
||||
make_repo()
|
||||
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(models.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
outside_file = Path(tmp_dname) / "outside.txt"
|
||||
outside_file.touch()
|
||||
|
||||
# This should not be allowed!
|
||||
# It was blowing up with GitCommandError, per:
|
||||
# https://github.com/paul-gauthier/aider/issues/178
|
||||
commands.cmd_add("../outside.txt")
|
||||
|
||||
self.assertEqual(len(coder.abs_fnames), 0)
|
||||
|
||||
def test_cmd_add_filename_with_special_chars(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(models.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("with[brackets].txt")
|
||||
fname.touch()
|
||||
|
||||
commands.cmd_add(str(fname))
|
||||
|
||||
self.assertIn(str(fname.resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_abs_filename(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(models.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("file.txt")
|
||||
fname.touch()
|
||||
|
||||
commands.cmd_add(str(fname.resolve()))
|
||||
|
||||
self.assertIn(str(fname.resolve()), coder.abs_fnames)
|
||||
|
||||
def test_cmd_add_quoted_filename(self):
|
||||
with ChdirTemporaryDirectory():
|
||||
io = InputOutput(pretty=False, yes=False)
|
||||
from aider.coders import Coder
|
||||
|
||||
coder = Coder.create(models.GPT35, None, io)
|
||||
commands = Commands(io, coder)
|
||||
|
||||
fname = Path("file with spaces.txt")
|
||||
fname.touch()
|
||||
|
||||
commands.cmd_add(f'"{fname}"')
|
||||
|
||||
self.assertIn(str(fname.resolve()), coder.abs_fnames)
|
||||
|
|
|
@ -68,7 +68,6 @@ class TestRepo(unittest.TestCase):
|
|||
|
||||
git_repo = GitRepo(InputOutput(), None, ".")
|
||||
diffs = git_repo.diff_commits(False, "HEAD~1", "HEAD")
|
||||
dump(diffs)
|
||||
self.assertIn("two", diffs)
|
||||
|
||||
@patch("aider.repo.simple_send_with_retries")
|
||||
|
@ -171,6 +170,55 @@ class TestRepo(unittest.TestCase):
|
|||
self.assertIn(str(fname), fnames)
|
||||
self.assertIn(str(fname2), fnames)
|
||||
|
||||
def test_get_tracked_files_with_aiderignore(self):
|
||||
with GitTemporaryDirectory():
|
||||
# new repo
|
||||
raw_repo = git.Repo()
|
||||
|
||||
# add it, but no commits at all in the raw_repo yet
|
||||
fname = Path("new.txt")
|
||||
fname.touch()
|
||||
raw_repo.git.add(str(fname))
|
||||
|
||||
aiderignore = Path(".aiderignore")
|
||||
git_repo = GitRepo(InputOutput(), None, None, str(aiderignore))
|
||||
|
||||
# better be there
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
|
||||
# commit it, better still be there
|
||||
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()
|
||||
raw_repo.git.add(str(fname2))
|
||||
|
||||
# both should be there
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
self.assertIn(str(fname2), fnames)
|
||||
|
||||
aiderignore.write_text("new.txt\n")
|
||||
|
||||
# new.txt should be gone!
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertNotIn(str(fname), fnames)
|
||||
self.assertIn(str(fname2), fnames)
|
||||
|
||||
# This does not work in github actions?!
|
||||
# The mtime doesn't change, even if I time.sleep(1)
|
||||
# Before doing this write_text()!?
|
||||
#
|
||||
# aiderignore.write_text("new2.txt\n")
|
||||
# new2.txt should be gone!
|
||||
# fnames = git_repo.get_tracked_files()
|
||||
# self.assertIn(str(fname), fnames)
|
||||
# self.assertNotIn(str(fname2), fnames)
|
||||
|
||||
def test_get_tracked_files_from_subdir(self):
|
||||
with GitTemporaryDirectory():
|
||||
# new repo
|
||||
|
@ -194,3 +242,21 @@ class TestRepo(unittest.TestCase):
|
|||
raw_repo.git.commit("-m", "new")
|
||||
fnames = git_repo.get_tracked_files()
|
||||
self.assertIn(str(fname), fnames)
|
||||
|
||||
@patch("aider.repo.simple_send_with_retries")
|
||||
def test_noop_commit(self, mock_send):
|
||||
mock_send.return_value = '"a good commit message"'
|
||||
|
||||
with GitTemporaryDirectory():
|
||||
# new repo
|
||||
raw_repo = git.Repo()
|
||||
|
||||
# add it, but no commits at all in the raw_repo yet
|
||||
fname = Path("file.txt")
|
||||
fname.touch()
|
||||
raw_repo.git.add(str(fname))
|
||||
raw_repo.git.commit("-m", "new")
|
||||
|
||||
git_repo = GitRepo(InputOutput(), None, None)
|
||||
|
||||
git_repo.commit(fnames=[str(fname)])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue