Skip to content

Add Repository.amend_commit #1098

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
merged 1 commit into from
Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pygit2/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
'net.h',
'refspec.h',
'repository.h',
'commit.h',
'revert.h',
'stash.h',
'submodule.h',
Expand Down
9 changes: 9 additions & 0 deletions pygit2/decl/commit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
int git_commit_amend(
git_oid *id,
const git_commit *commit_to_amend,
const char *update_ref,
const git_signature *author,
const git_signature *committer,
const char *message_encoding,
const char *message,
const git_tree *tree);
115 changes: 114 additions & 1 deletion pygit2/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from ._pygit2 import GIT_FILEMODE_LINK
from ._pygit2 import GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE, GIT_BRANCH_ALL
from ._pygit2 import GIT_REF_SYMBOLIC
from ._pygit2 import Reference, Tree, Commit, Blob
from ._pygit2 import Reference, Tree, Commit, Blob, Signature
from ._pygit2 import InvalidSpecError

from .callbacks import git_fetch_options
Expand Down Expand Up @@ -1364,6 +1364,119 @@ def revert_commit(self, revert_commit, our_commit, mainline=0):

return Index.from_c(self, cindex)

#
# Amend commit
#
def amend_commit(self, commit, refname, author=None,
committer=None, message=None, tree=None,
encoding='UTF-8'):
"""
Amend an existing commit by replacing only explicitly passed values,
return the rewritten commit's oid.

This creates a new commit that is exactly the same as the old commit,
except that any explicitly passed values will be updated. The new
commit has the same parents as the old commit.

You may omit the `author`, `committer`, `message`, `tree`, and
`encoding` parameters, in which case this will use the values
from the original `commit`.

Parameters:

commit : Commit, Oid, or str
The commit to amend.

refname : Reference or str
If not `None`, name of the reference that will be updated to point
to the newly rewritten commit. Use "HEAD" to update the HEAD of the
current branch and make it point to the rewritten commit.
If you want to amend a commit that is not currently the tip of the
branch and then rewrite the following commits to reach a ref, pass
this as `None` and update the rest of the commit chain and ref
separately.

author : Signature
If not None, replace the old commit's author signature with this
one.

committer : Signature
If not None, replace the old commit's committer signature with this
one.

message : str
If not None, replace the old commit's message with this one.

tree : Tree, Oid, or str
If not None, replace the old commit's tree with this one.

encoding : str
Optional encoding for `message`.
"""

# Initialize parameters to pass on to C function git_commit_amend.
# Note: the pointers are all initialized to NULL by default.
coid = ffi.new('git_oid *')
commit_cptr = ffi.new('git_commit **')
refname_cstr = ffi.NULL
author_cptr = ffi.new('git_signature **')
committer_cptr = ffi.new('git_signature **')
message_cstr = ffi.NULL
encoding_cstr = ffi.NULL
tree_cptr = ffi.new('git_tree **')

# Get commit as pointer to git_commit.
if isinstance(commit, (str, Oid)):
commit = self[commit]
elif isinstance(commit, Commit):
pass
elif commit is None:
raise ValueError("the commit to amend cannot be None")
else:
raise TypeError("the commit to amend must be a Commit, str, or Oid")
commit = commit.peel(Commit)
ffi.buffer(commit_cptr)[:] = commit._pointer[:]

# Get refname as C string.
if isinstance(refname, Reference):
refname_cstr = ffi.new('char[]', to_bytes(refname.name))
elif type(refname) is str:
refname_cstr = ffi.new('char[]', to_bytes(refname))
elif refname is not None:
raise TypeError("refname must be a str or Reference")

# Get author as pointer to git_signature.
if isinstance(author, Signature):
ffi.buffer(author_cptr)[:] = author._pointer[:]
elif author is not None:
raise TypeError("author must be a Signature")

# Get committer as pointer to git_signature.
if isinstance(committer, Signature):
ffi.buffer(committer_cptr)[:] = committer._pointer[:]
elif committer is not None:
raise TypeError("committer must be a Signature")

# Get message and encoding as C strings.
if message is not None:
message_cstr = ffi.new('char[]', to_bytes(message, encoding))
encoding_cstr = ffi.new('char[]', to_bytes(encoding))

# Get tree as pointer to git_tree.
if tree is not None:
if isinstance(tree, (str, Oid)):
tree = self[tree]
tree = tree.peel(Tree)
ffi.buffer(tree_cptr)[:] = tree._pointer[:]

# Amend the commit.
err = C.git_commit_amend(coid, commit_cptr[0], refname_cstr,
author_cptr[0], committer_cptr[0],
encoding_cstr, message_cstr, tree_cptr[0])
check_error(err)

return Oid(raw=bytes(ffi.buffer(coid)[:]))


class Branches:

Expand Down
116 changes: 115 additions & 1 deletion test/test_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@

import pytest

from pygit2 import GIT_OBJ_COMMIT, Signature, Oid
from pygit2 import GIT_OBJ_COMMIT, Signature, Oid, GitError
from . import utils


COMMIT_SHA = '5fe808e8953c12735680c257f56600cb0de44b10'
COMMIT_SHA_TO_AMEND = '784855caf26449a1914d2cf62d12b9374d76ae78' # tip of the master branch


@utils.refcount
Expand Down Expand Up @@ -133,3 +134,116 @@ def test_modify_commit(barerepo):
with pytest.raises(AttributeError): setattr(commit, 'author', author)
with pytest.raises(AttributeError): setattr(commit, 'tree', None)
with pytest.raises(AttributeError): setattr(commit, 'parents', None)

def test_amend_commit_metadata(barerepo):
repo = barerepo
commit = repo[COMMIT_SHA_TO_AMEND]
assert commit.oid == repo.head.target

encoding = 'iso-8859-1'
amended_message = "Amended commit message.\n\nMessage with non-ascii chars: ééé.\n"
amended_author = Signature('Jane Author', '[email protected]', 12345, 0)
amended_committer = Signature('John Committer', '[email protected]', 12346, 0)

amended_oid = repo.amend_commit(
commit, 'HEAD', message=amended_message, author=amended_author,
committer=amended_committer, encoding=encoding)
amended_commit = repo[amended_oid]

assert repo.head.target == amended_oid
assert GIT_OBJ_COMMIT == amended_commit.type
assert amended_committer == amended_commit.committer
assert amended_author == amended_commit.author
assert amended_message.encode(encoding) == amended_commit.raw_message
assert commit.author != amended_commit.author
assert commit.committer != amended_commit.committer
assert commit.tree == amended_commit.tree # we didn't touch the tree

def test_amend_commit_tree(barerepo):
repo = barerepo
commit = repo[COMMIT_SHA_TO_AMEND]
assert commit.oid == repo.head.target

tree = '967fce8df97cc71722d3c2a5930ef3e6f1d27b12'
tree_prefix = tree[:5]

amended_oid = repo.amend_commit(commit, 'HEAD', tree=tree_prefix)
amended_commit = repo[amended_oid]

assert repo.head.target == amended_oid
assert GIT_OBJ_COMMIT == amended_commit.type
assert commit.message == amended_commit.message
assert commit.author == amended_commit.author
assert commit.committer == amended_commit.committer
assert commit.tree_id != amended_commit.tree_id
assert Oid(hex=tree) == amended_commit.tree_id

def test_amend_commit_not_tip_of_branch(barerepo):
repo = barerepo

# This commit isn't at the tip of the branch.
commit = repo['5fe808e8953c12735680c257f56600cb0de44b10']
assert commit.oid != repo.head.target

# Can't update HEAD to the rewritten commit because it's not the tip of the branch.
with pytest.raises(GitError):
repo.amend_commit(commit, 'HEAD', message="this won't work!")

# We can still amend the commit if we don't try to update a ref.
repo.amend_commit(commit, None, message="this will work")

def test_amend_commit_no_op(barerepo):
repo = barerepo
commit = repo[COMMIT_SHA_TO_AMEND]
assert commit.oid == repo.head.target

amended_oid = repo.amend_commit(commit, None)
assert amended_oid == commit.oid

def test_amend_commit_argument_types(barerepo):
repo = barerepo

some_tree = repo['967fce8df97cc71722d3c2a5930ef3e6f1d27b12']
commit = repo[COMMIT_SHA_TO_AMEND]
alt_commit1 = Oid(hex=COMMIT_SHA_TO_AMEND)
alt_commit2 = COMMIT_SHA_TO_AMEND
alt_tree = some_tree
alt_refname = repo.head # try this one last, because it'll change the commit at the tip

# Pass bad values/types for the commit
with pytest.raises(ValueError): repo.amend_commit(None, None)
with pytest.raises(TypeError): repo.amend_commit(some_tree, None)

# Pass bad types for signatures
with pytest.raises(TypeError): repo.amend_commit(commit, None, author="Toto")
with pytest.raises(TypeError): repo.amend_commit(commit, None, committer="Toto")

# Pass bad refnames
with pytest.raises(ValueError): repo.amend_commit(commit, "this-ref-doesnt-exist")
with pytest.raises(TypeError): repo.amend_commit(commit, repo)

# Pass bad trees
with pytest.raises(ValueError): repo.amend_commit(commit, None, tree="can't parse this")
with pytest.raises(KeyError): repo.amend_commit(commit, None, tree="baaaaad")

# Pass an Oid for the commit
amended_oid = repo.amend_commit(alt_commit1, None, message="Hello")
amended_commit = repo[amended_oid]
assert GIT_OBJ_COMMIT == amended_commit.type
assert str(amended_oid) != COMMIT_SHA_TO_AMEND

# Pass a str for the commit
amended_oid = repo.amend_commit(alt_commit2, None, message="Hello", tree=alt_tree)
amended_commit = repo[amended_oid]
assert GIT_OBJ_COMMIT == amended_commit.type
assert str(amended_oid) != COMMIT_SHA_TO_AMEND
assert repo[COMMIT_SHA_TO_AMEND].tree != amended_commit.tree
assert alt_tree.oid == amended_commit.tree_id

# Pass an actual reference object for refname
# (Warning: the tip of the branch will be altered after this test!)
amended_oid = repo.amend_commit(alt_commit2, alt_refname, message="Hello")
amended_commit = repo[amended_oid]
assert GIT_OBJ_COMMIT == amended_commit.type
assert str(amended_oid) != COMMIT_SHA_TO_AMEND
assert repo.head.target == amended_oid