Skip to content

Commit 98e3d1f

Browse files
committed
Add Repository.amend_commit
1 parent d94a630 commit 98e3d1f

File tree

4 files changed

+239
-2
lines changed

4 files changed

+239
-2
lines changed

pygit2/_run.py

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
'net.h',
7878
'refspec.h',
7979
'repository.h',
80+
'commit.h',
8081
'revert.h',
8182
'stash.h',
8283
'submodule.h',

pygit2/decl/commit.h

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
int git_commit_amend(
2+
git_oid *id,
3+
const git_commit *commit_to_amend,
4+
const char *update_ref,
5+
const git_signature *author,
6+
const git_signature *committer,
7+
const char *message_encoding,
8+
const char *message,
9+
const git_tree *tree);

pygit2/repository.py

+114-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from ._pygit2 import GIT_FILEMODE_LINK
3838
from ._pygit2 import GIT_BRANCH_LOCAL, GIT_BRANCH_REMOTE, GIT_BRANCH_ALL
3939
from ._pygit2 import GIT_REF_SYMBOLIC
40-
from ._pygit2 import Reference, Tree, Commit, Blob
40+
from ._pygit2 import Reference, Tree, Commit, Blob, Signature
4141
from ._pygit2 import InvalidSpecError
4242

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

13651365
return Index.from_c(self, cindex)
13661366

1367+
#
1368+
# Amend commit
1369+
#
1370+
def amend_commit(self, commit, refname, author=None,
1371+
committer=None, message=None, tree=None,
1372+
encoding='UTF-8'):
1373+
"""
1374+
Amend an existing commit by replacing only explicitly passed values,
1375+
return the rewritten commit's oid.
1376+
1377+
This creates a new commit that is exactly the same as the old commit,
1378+
except that any explicitly passed values will be updated. The new
1379+
commit has the same parents as the old commit.
1380+
1381+
You may omit the `author`, `committer`, `message`, `tree`, and
1382+
`encoding` parameters, in which case this will use the values
1383+
from the original `commit`.
1384+
1385+
Parameters:
1386+
1387+
commit : Commit, Oid, or str
1388+
The commit to amend.
1389+
1390+
refname : Reference or str
1391+
If not `None`, name of the reference that will be updated to point
1392+
to the newly rewritten commit. Use "HEAD" to update the HEAD of the
1393+
current branch and make it point to the rewritten commit.
1394+
If you want to amend a commit that is not currently the tip of the
1395+
branch and then rewrite the following commits to reach a ref, pass
1396+
this as `None` and update the rest of the commit chain and ref
1397+
separately.
1398+
1399+
author : Signature
1400+
If not None, replace the old commit's author signature with this
1401+
one.
1402+
1403+
committer : Signature
1404+
If not None, replace the old commit's committer signature with this
1405+
one.
1406+
1407+
message : str
1408+
If not None, replace the old commit's message with this one.
1409+
1410+
tree : Tree, Oid, or str
1411+
If not None, replace the old commit's tree with this one.
1412+
1413+
encoding : str
1414+
Optional encoding for `message`.
1415+
"""
1416+
1417+
# Initialize parameters to pass on to C function git_commit_amend.
1418+
# Note: the pointers are all initialized to NULL by default.
1419+
coid = ffi.new('git_oid *')
1420+
commit_cptr = ffi.new('git_commit **')
1421+
refname_cstr = ffi.NULL
1422+
author_cptr = ffi.new('git_signature **')
1423+
committer_cptr = ffi.new('git_signature **')
1424+
message_cstr = ffi.NULL
1425+
encoding_cstr = ffi.NULL
1426+
tree_cptr = ffi.new('git_tree **')
1427+
1428+
# Get commit as pointer to git_commit.
1429+
if isinstance(commit, (str, Oid)):
1430+
commit = self[commit]
1431+
elif isinstance(commit, Commit):
1432+
pass
1433+
elif commit is None:
1434+
raise ValueError("the commit to amend cannot be None")
1435+
else:
1436+
raise TypeError("the commit to amend must be a Commit, str, or Oid")
1437+
commit = commit.peel(Commit)
1438+
ffi.buffer(commit_cptr)[:] = commit._pointer[:]
1439+
1440+
# Get refname as C string.
1441+
if isinstance(refname, Reference):
1442+
refname_cstr = ffi.new('char[]', to_bytes(refname.name))
1443+
elif type(refname) is str:
1444+
refname_cstr = ffi.new('char[]', to_bytes(refname))
1445+
elif refname is not None:
1446+
raise TypeError("refname must be a str or Reference")
1447+
1448+
# Get author as pointer to git_signature.
1449+
if isinstance(author, Signature):
1450+
ffi.buffer(author_cptr)[:] = author._pointer[:]
1451+
elif author is not None:
1452+
raise TypeError("author must be a Signature")
1453+
1454+
# Get committer as pointer to git_signature.
1455+
if isinstance(committer, Signature):
1456+
ffi.buffer(committer_cptr)[:] = committer._pointer[:]
1457+
elif committer is not None:
1458+
raise TypeError("committer must be a Signature")
1459+
1460+
# Get message and encoding as C strings.
1461+
if message is not None:
1462+
message_cstr = ffi.new('char[]', to_bytes(message, encoding))
1463+
encoding_cstr = ffi.new('char[]', to_bytes(encoding))
1464+
1465+
# Get tree as pointer to git_tree.
1466+
if tree is not None:
1467+
if isinstance(tree, (str, Oid)):
1468+
tree = self[tree]
1469+
tree = tree.peel(Tree)
1470+
ffi.buffer(tree_cptr)[:] = tree._pointer[:]
1471+
1472+
# Amend the commit.
1473+
err = C.git_commit_amend(coid, commit_cptr[0], refname_cstr,
1474+
author_cptr[0], committer_cptr[0],
1475+
encoding_cstr, message_cstr, tree_cptr[0])
1476+
check_error(err)
1477+
1478+
return Oid(raw=bytes(ffi.buffer(coid)[:]))
1479+
13671480

13681481
class Branches:
13691482

test/test_commit.py

+115-1
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@
2929

3030
import pytest
3131

32-
from pygit2 import GIT_OBJ_COMMIT, Signature, Oid
32+
from pygit2 import GIT_OBJ_COMMIT, Signature, Oid, GitError
3333
from . import utils
3434

3535

3636
COMMIT_SHA = '5fe808e8953c12735680c257f56600cb0de44b10'
37+
COMMIT_SHA_TO_AMEND = '784855caf26449a1914d2cf62d12b9374d76ae78' # tip of the master branch
3738

3839

3940
@utils.refcount
@@ -133,3 +134,116 @@ def test_modify_commit(barerepo):
133134
with pytest.raises(AttributeError): setattr(commit, 'author', author)
134135
with pytest.raises(AttributeError): setattr(commit, 'tree', None)
135136
with pytest.raises(AttributeError): setattr(commit, 'parents', None)
137+
138+
def test_amend_commit_metadata(barerepo):
139+
repo = barerepo
140+
commit = repo[COMMIT_SHA_TO_AMEND]
141+
assert commit.oid == repo.head.target
142+
143+
encoding = 'iso-8859-1'
144+
amended_message = "Amended commit message.\n\nMessage with non-ascii chars: ééé.\n"
145+
amended_author = Signature('Jane Author', '[email protected]', 12345, 0)
146+
amended_committer = Signature('John Committer', '[email protected]', 12346, 0)
147+
148+
amended_oid = repo.amend_commit(
149+
commit, 'HEAD', message=amended_message, author=amended_author,
150+
committer=amended_committer, encoding=encoding)
151+
amended_commit = repo[amended_oid]
152+
153+
assert repo.head.target == amended_oid
154+
assert GIT_OBJ_COMMIT == amended_commit.type
155+
assert amended_committer == amended_commit.committer
156+
assert amended_author == amended_commit.author
157+
assert amended_message.encode(encoding) == amended_commit.raw_message
158+
assert commit.author != amended_commit.author
159+
assert commit.committer != amended_commit.committer
160+
assert commit.tree == amended_commit.tree # we didn't touch the tree
161+
162+
def test_amend_commit_tree(barerepo):
163+
repo = barerepo
164+
commit = repo[COMMIT_SHA_TO_AMEND]
165+
assert commit.oid == repo.head.target
166+
167+
tree = '967fce8df97cc71722d3c2a5930ef3e6f1d27b12'
168+
tree_prefix = tree[:5]
169+
170+
amended_oid = repo.amend_commit(commit, 'HEAD', tree=tree_prefix)
171+
amended_commit = repo[amended_oid]
172+
173+
assert repo.head.target == amended_oid
174+
assert GIT_OBJ_COMMIT == amended_commit.type
175+
assert commit.message == amended_commit.message
176+
assert commit.author == amended_commit.author
177+
assert commit.committer == amended_commit.committer
178+
assert commit.tree_id != amended_commit.tree_id
179+
assert Oid(hex=tree) == amended_commit.tree_id
180+
181+
def test_amend_commit_not_tip_of_branch(barerepo):
182+
repo = barerepo
183+
184+
# This commit isn't at the tip of the branch.
185+
commit = repo['5fe808e8953c12735680c257f56600cb0de44b10']
186+
assert commit.oid != repo.head.target
187+
188+
# Can't update HEAD to the rewritten commit because it's not the tip of the branch.
189+
with pytest.raises(GitError):
190+
repo.amend_commit(commit, 'HEAD', message="this won't work!")
191+
192+
# We can still amend the commit if we don't try to update a ref.
193+
repo.amend_commit(commit, None, message="this will work")
194+
195+
def test_amend_commit_no_op(barerepo):
196+
repo = barerepo
197+
commit = repo[COMMIT_SHA_TO_AMEND]
198+
assert commit.oid == repo.head.target
199+
200+
amended_oid = repo.amend_commit(commit, None)
201+
assert amended_oid == commit.oid
202+
203+
def test_amend_commit_argument_types(barerepo):
204+
repo = barerepo
205+
206+
some_tree = repo['967fce8df97cc71722d3c2a5930ef3e6f1d27b12']
207+
commit = repo[COMMIT_SHA_TO_AMEND]
208+
alt_commit1 = Oid(hex=COMMIT_SHA_TO_AMEND)
209+
alt_commit2 = COMMIT_SHA_TO_AMEND
210+
alt_tree = some_tree
211+
alt_refname = repo.head # try this one last, because it'll change the commit at the tip
212+
213+
# Pass bad values/types for the commit
214+
with pytest.raises(ValueError): repo.amend_commit(None, None)
215+
with pytest.raises(TypeError): repo.amend_commit(some_tree, None)
216+
217+
# Pass bad types for signatures
218+
with pytest.raises(TypeError): repo.amend_commit(commit, None, author="Toto")
219+
with pytest.raises(TypeError): repo.amend_commit(commit, None, committer="Toto")
220+
221+
# Pass bad refnames
222+
with pytest.raises(ValueError): repo.amend_commit(commit, "this-ref-doesnt-exist")
223+
with pytest.raises(TypeError): repo.amend_commit(commit, repo)
224+
225+
# Pass bad trees
226+
with pytest.raises(ValueError): repo.amend_commit(commit, None, tree="can't parse this")
227+
with pytest.raises(KeyError): repo.amend_commit(commit, None, tree="baaaaad")
228+
229+
# Pass an Oid for the commit
230+
amended_oid = repo.amend_commit(alt_commit1, None, message="Hello")
231+
amended_commit = repo[amended_oid]
232+
assert GIT_OBJ_COMMIT == amended_commit.type
233+
assert str(amended_oid) != COMMIT_SHA_TO_AMEND
234+
235+
# Pass a str for the commit
236+
amended_oid = repo.amend_commit(alt_commit2, None, message="Hello", tree=alt_tree)
237+
amended_commit = repo[amended_oid]
238+
assert GIT_OBJ_COMMIT == amended_commit.type
239+
assert str(amended_oid) != COMMIT_SHA_TO_AMEND
240+
assert repo[COMMIT_SHA_TO_AMEND].tree != amended_commit.tree
241+
assert alt_tree.oid == amended_commit.tree_id
242+
243+
# Pass an actual reference object for refname
244+
# (Warning: the tip of the branch will be altered after this test!)
245+
amended_oid = repo.amend_commit(alt_commit2, alt_refname, message="Hello")
246+
amended_commit = repo[amended_oid]
247+
assert GIT_OBJ_COMMIT == amended_commit.type
248+
assert str(amended_oid) != COMMIT_SHA_TO_AMEND
249+
assert repo.head.target == amended_oid

0 commit comments

Comments
 (0)