-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat: Add Co-authored-by attribution for AI commits #3789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
paul-gauthier
merged 44 commits into
Aider-AI:main
from
ei-grad:attribute-co-authored-by
May 7, 2025
Merged
Changes from 41 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
67bb4f9
feat: add co-authored-by commit attribution
ei-grad b6b8f30
test: add tests for co-authored-by commit attribution
ei-grad eb28e22
test: fix mock setup for model name in co-authored-by test
ei-grad 192f8be
test: fix mock model name setup in co-authored-by test
ei-grad a5327af
test: fix mock setup for co-authored-by commit test
ei-grad b22c9b8
feat: implement Co-authored-by attribution option
ei-grad c73b987
fix: fix syntax error in commit logic
ei-grad e951164
chore: Add test comment to dump function
ei-grad 482e0c2
chore: Add test comment
ei-grad 4783ad3
feat: add attribute-co-authored-by option for commit attribution
ei-grad 43cb4d6
test: Temporarily disable co-author attribution to verify test failure
ei-grad dede701
test: intentionally break co-authored-by logic
ei-grad 80114e7
chore: revert intentional break introduced for testing
ei-grad d5671c2
chore: Add test comment
ei-grad 48f89f2
fix: prevent name modification when using co-authored-by
ei-grad 072bd30
test: add comment for testing
ei-grad f648a01
fix: Pass attribute_co_authored_by arg to GitRepo constructor
ei-grad ff8e985
chore: add test comment to dump function
ei-grad d1437b7
chore: add debug prints for attribute_co_authored_by
ei-grad 15d623f
chore: add another test comment to prompts
ei-grad 316d8f8
chore: add third test comment
ei-grad 66fdece
Revert "chore: add third test comment"
ei-grad 0a59c38
Revert "chore: add another test comment to prompts"
ei-grad e182052
Revert "chore: add debug prints for attribute_co_authored_by"
ei-grad 02bc9a8
Revert "chore: add test comment to dump function"
ei-grad cf7b35f
Revert "test: add comment for testing"
ei-grad 7b8c7ed
Revert "chore: Add test comment"
ei-grad aa07e16
Revert "chore: Add test comment"
ei-grad 427f9c5
Revert "chore: Add test comment to dump function"
ei-grad c56e836
refactor: simplify commit logic and use context manager for git env
ei-grad dd4b61d
test: add test for co-authored-by precedence over author/committer
ei-grad ea74f31
feat: Explicit author/committer flags override co-authored-by
ei-grad 278a596
docs: clarify commit author/committer/co-authored-by logic
ei-grad 5664b5b
test: Assert commit return value in more tests
ei-grad 37a2527
test: Fix commit result assertion in test_noop_commit
ei-grad d991cb6
test: cover user commit with no committer attribution
ei-grad 3e1bc77
test: add tests for commit author/committer attribution logic
ei-grad 9e91e8f
test: remove redundant commit attribution tests
ei-grad 6a970c3
test: remove redundant co-authored-by precedence test
ei-grad 5851d66
test: improve test clarity with skipIf and assertion messages
ei-grad 38dfd6f
docs: clarify --attribute-co-authored-by precedence
ei-grad 165e237
chore: remove unnecessary comment in repo.py
ei-grad 3f94fd5
refactor: Simplify access to attribute_co_authored_by
ei-grad 1d42690
fix: update co-authored-by domain to aider.chat
ei-grad File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import contextlib | ||
import os | ||
import time | ||
from pathlib import Path, PurePosixPath | ||
|
@@ -34,6 +35,19 @@ | |
ANY_GIT_ERROR = tuple(ANY_GIT_ERROR) | ||
|
||
|
||
@contextlib.contextmanager | ||
def set_git_env(var_name, value, original_value): | ||
"""Temporarily set a Git environment variable.""" | ||
os.environ[var_name] = value | ||
try: | ||
yield | ||
finally: | ||
if original_value is not None: | ||
os.environ[var_name] = original_value | ||
elif var_name in os.environ: | ||
del os.environ[var_name] | ||
|
||
|
||
class GitRepo: | ||
repo = None | ||
aider_ignore_file = None | ||
|
@@ -58,6 +72,7 @@ def __init__( | |
commit_prompt=None, | ||
subtree_only=False, | ||
git_commit_verify=True, | ||
attribute_co_authored_by=False, # Added parameter | ||
): | ||
self.io = io | ||
self.models = models | ||
|
@@ -69,6 +84,7 @@ def __init__( | |
self.attribute_committer = attribute_committer | ||
self.attribute_commit_message_author = attribute_commit_message_author | ||
self.attribute_commit_message_committer = attribute_commit_message_committer | ||
self.attribute_co_authored_by = attribute_co_authored_by # Assign from parameter | ||
self.commit_prompt = commit_prompt | ||
self.subtree_only = subtree_only | ||
self.git_commit_verify = git_commit_verify | ||
|
@@ -111,7 +127,71 @@ def __init__( | |
if aider_ignore_file: | ||
self.aider_ignore_file = Path(aider_ignore_file) | ||
|
||
def commit(self, fnames=None, context=None, message=None, aider_edits=False): | ||
def commit(self, fnames=None, context=None, message=None, aider_edits=False, coder=None): | ||
""" | ||
Commit the specified files or all dirty files if none are specified. | ||
|
||
Args: | ||
fnames (list, optional): List of filenames to commit. Defaults to None (commit all | ||
dirty files). | ||
context (str, optional): Context for generating the commit message. Defaults to None. | ||
message (str, optional): Explicit commit message. Defaults to None (generate message). | ||
aider_edits (bool, optional): Whether the changes were made by Aider. Defaults to False. | ||
This affects attribution logic. | ||
coder (Coder, optional): The Coder instance, used to access config and model info. | ||
Defaults to None. | ||
|
||
Returns: | ||
tuple(str, str) or None: The commit hash and commit message if successful, else None. | ||
|
||
Attribution Logic: | ||
------------------ | ||
This method handles Git commit attribution based on configuration flags and whether | ||
Aider generated the changes (`aider_edits`). | ||
|
||
Key Concepts: | ||
- Author: The person who originally wrote the code changes. | ||
- Committer: The person who last applied the commit to the repository. | ||
- aider_edits=True: Changes were generated by Aider (LLM). | ||
- aider_edits=False: Commit is user-driven (e.g., /commit manually staged changes). | ||
- Explicit Setting: A flag (--attribute-...) is set to True or False via command line | ||
or config file. | ||
- Implicit Default: A flag is not explicitly set, defaulting to None in args, which is | ||
interpreted as True unless overridden by other logic. | ||
|
||
Flags: | ||
- --attribute-author: Modify Author name to "User Name (aider)". | ||
- --attribute-committer: Modify Committer name to "User Name (aider)". | ||
- --attribute-co-authored-by: Add "Co-authored-by: aider (<model>) <[email protected]>" | ||
trailer to the commit message. | ||
|
||
Behavior Summary: | ||
|
||
1. When aider_edits = True (AI Changes): | ||
- If --attribute-co-authored-by=True: | ||
- Co-authored-by trailer IS ADDED. | ||
- Author/Committer names are NOT modified by default (co-authored-by takes precedence). | ||
- EXCEPTION: If --attribute-author/--attribute-committer is EXPLICITLY True, | ||
the respective name IS modified (explicit overrides precedence). | ||
- If --attribute-co-authored-by=False: | ||
- Co-authored-by trailer is NOT added. | ||
- Author/Committer names ARE modified by default (implicit True). | ||
- EXCEPTION: If --attribute-author/--attribute-committer is EXPLICITLY False, | ||
the respective name is NOT modified. | ||
|
||
2. When aider_edits = False (User Changes): | ||
- --attribute-co-authored-by is IGNORED (trailer never added). | ||
- Author name is NEVER modified (--attribute-author ignored). | ||
- Committer name IS modified by default (implicit True, as Aider runs `git commit`). | ||
- EXCEPTION: If --attribute-committer is EXPLICITLY False, the name is NOT modified. | ||
|
||
Resulting Scenarios: | ||
- Standard AI edit (defaults): Co-authored-by=False -> Author=You(aider), Committer=You(aider) | ||
- AI edit with Co-authored-by (default): Co-authored-by=True -> Author=You, Committer=You, Trailer added | ||
- AI edit with Co-authored-by + Explicit Author: Co-authored-by=True, --attribute-author -> Author=You(aider), Committer=You, Trailer added | ||
- User commit (defaults): aider_edits=False -> Author=You, Committer=You(aider) | ||
- User commit with explicit no-committer: aider_edits=False, --no-attribute-committer -> Author=You, Committer=You | ||
""" | ||
if not fnames and not self.repo.is_dirty(): | ||
return | ||
|
||
|
@@ -124,17 +204,68 @@ def commit(self, fnames=None, context=None, message=None, aider_edits=False): | |
else: | ||
commit_message = self.get_commit_message(diffs, context) | ||
|
||
if aider_edits and self.attribute_commit_message_author: | ||
commit_message = "aider: " + commit_message | ||
elif self.attribute_commit_message_committer: | ||
commit_message = "aider: " + commit_message | ||
# Retrieve attribute settings, prioritizing coder.args if available | ||
if coder and hasattr(coder, "args"): | ||
attribute_author = coder.args.attribute_author | ||
attribute_committer = coder.args.attribute_committer | ||
attribute_commit_message_author = coder.args.attribute_commit_message_author | ||
attribute_commit_message_committer = coder.args.attribute_commit_message_committer | ||
attribute_co_authored_by = coder.args.attribute_co_authored_by # <-- Restored | ||
else: | ||
# Fallback to self attributes (initialized from config/defaults) | ||
attribute_author = self.attribute_author | ||
attribute_committer = self.attribute_committer | ||
attribute_commit_message_author = self.attribute_commit_message_author | ||
attribute_commit_message_committer = self.attribute_commit_message_committer | ||
attribute_co_authored_by = getattr(self, "attribute_co_authored_by", False) # Should be False if not set | ||
ei-grad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Determine explicit settings (None means use default behavior) | ||
author_explicit = attribute_author is not None | ||
committer_explicit = attribute_committer is not None | ||
|
||
# Determine effective settings (apply default True if not explicit) | ||
effective_author = True if attribute_author is None else attribute_author | ||
effective_committer = True if attribute_committer is None else attribute_committer | ||
|
||
|
||
# Determine commit message prefixing | ||
prefix_commit_message = aider_edits and ( | ||
attribute_commit_message_author or attribute_commit_message_committer | ||
) | ||
|
||
# Determine Co-authored-by trailer | ||
commit_message_trailer = "" | ||
if aider_edits and attribute_co_authored_by: | ||
model_name = "unknown-model" | ||
if coder and hasattr(coder, "main_model") and coder.main_model.name: | ||
model_name = coder.main_model.name | ||
commit_message_trailer = ( | ||
f"\n\nCo-authored-by: aider ({model_name}) <[email protected]>" | ||
ei-grad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
# Determine if author/committer names should be modified | ||
# Author modification applies only to aider edits. | ||
# It's used if effective_author is True AND (co-authored-by is False OR author was explicitly set). | ||
use_attribute_author = ( | ||
aider_edits | ||
and effective_author | ||
and (not attribute_co_authored_by or author_explicit) | ||
) | ||
|
||
# Committer modification applies regardless of aider_edits (based on tests). | ||
# It's used if effective_committer is True AND (it's not an aider edit with co-authored-by OR committer was explicitly set). | ||
use_attribute_committer = effective_committer and ( | ||
not (aider_edits and attribute_co_authored_by) or committer_explicit | ||
) | ||
|
||
|
||
if not commit_message: | ||
commit_message = "(no commit message provided)" | ||
|
||
full_commit_message = commit_message | ||
# if context: | ||
# full_commit_message += "\n\n# Aider chat conversation:\n\n" + context | ||
if prefix_commit_message: | ||
commit_message = "aider: " + commit_message | ||
|
||
full_commit_message = commit_message + commit_message_trailer | ||
|
||
cmd = ["-m", full_commit_message] | ||
if not self.git_commit_verify: | ||
|
@@ -152,36 +283,30 @@ def commit(self, fnames=None, context=None, message=None, aider_edits=False): | |
|
||
original_user_name = self.repo.git.config("--get", "user.name") | ||
original_committer_name_env = os.environ.get("GIT_COMMITTER_NAME") | ||
original_author_name_env = os.environ.get("GIT_AUTHOR_NAME") | ||
committer_name = f"{original_user_name} (aider)" | ||
|
||
if self.attribute_committer: | ||
os.environ["GIT_COMMITTER_NAME"] = committer_name | ||
|
||
if aider_edits and self.attribute_author: | ||
original_author_name_env = os.environ.get("GIT_AUTHOR_NAME") | ||
os.environ["GIT_AUTHOR_NAME"] = committer_name | ||
|
||
try: | ||
self.repo.git.commit(cmd) | ||
commit_hash = self.get_head_commit_sha(short=True) | ||
self.io.tool_output(f"Commit {commit_hash} {commit_message}", bold=True) | ||
return commit_hash, commit_message | ||
# Use context managers to handle environment variables | ||
with contextlib.ExitStack() as stack: | ||
if use_attribute_committer: | ||
stack.enter_context( | ||
set_git_env("GIT_COMMITTER_NAME", committer_name, original_committer_name_env) | ||
) | ||
if use_attribute_author: | ||
stack.enter_context( | ||
set_git_env("GIT_AUTHOR_NAME", committer_name, original_author_name_env) | ||
) | ||
|
||
# Perform the commit | ||
self.repo.git.commit(cmd) | ||
commit_hash = self.get_head_commit_sha(short=True) | ||
self.io.tool_output(f"Commit {commit_hash} {commit_message}", bold=True) | ||
return commit_hash, commit_message | ||
|
||
except ANY_GIT_ERROR as err: | ||
self.io.tool_error(f"Unable to commit: {err}") | ||
finally: | ||
# Restore the env | ||
|
||
if self.attribute_committer: | ||
if original_committer_name_env is not None: | ||
os.environ["GIT_COMMITTER_NAME"] = original_committer_name_env | ||
else: | ||
del os.environ["GIT_COMMITTER_NAME"] | ||
|
||
if aider_edits and self.attribute_author: | ||
if original_author_name_env is not None: | ||
os.environ["GIT_AUTHOR_NAME"] = original_author_name_env | ||
else: | ||
del os.environ["GIT_AUTHOR_NAME"] | ||
# No return here, implicitly returns None | ||
ei-grad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def get_rel_repo_dir(self): | ||
try: | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.