英文:
How to remove a specific commit using pygit2?
问题
I'm stuck trying to implement a method with pygit2
that removes a given commit.
With plain git, I could do something like:
git rebase --onto SHA^ SHA
but there is no such method in the library.
According to pygit2 documentation, merge_trees
can be used to this end. So far, I got:
def deleteCommit(repo, commitId):
delete_commit = repo[commitId]
head = repo.head
base = repo.merge_base(delete_commit.id, head.target)
delete_commit_tree = repo.get(delete_commit).tree
head_tree = repo.get(head.target).tree
base_tree = repo.get(base).tree
index = repo.merge_trees(base_tree, head_tree, delete_commit_tree)
tree_id = index.write_tree(repo)
From what I have looked up in the documentation, this is probably the most straightforward way, aside from doing some repo.walk()
stuff which I couldn't really understand that well.
I came up with this new partial solution using repo.walk()
:
def deleteCommit(repo, commitId):
# Get the branch to be rewritten
branch = repo.lookup_branch(repo.head.shorthand)
branch_ref = repo.lookup_reference(branch.name)
# Get the commit history up to the branch tip
commits = list(repo.walk(branch.target, GIT_SORT_TOPOLOGICAL))
# Create a new tree with the desired changes
tree = repo.TreeBuilder().write()
for commit in commits:
if commitId != str(commit.id):
commitOid = git.Oid(hex=commitId)
if commitOid in commit.parent_ids:
commit.parent_ids.remove(commitOid)
repo.create_commit(
branch_ref.name, # no reflog message
commit.author, # use the default signature
commit.committer, # use the default signature
commit.message, # commit message
tree, # use the new tree
commit.parent_ids,
)
else:
repo.references.delete(f'refs/commits/{commit.hex}')
# Point the branch to the new commit
branch_ref.set_target(tree)
But that yields the error pygit2.GitError: failed to create commit: current tip is not the first parent
.
英文:
I'm stuck trying to implement a method with pygit2
that removes a given commit.
With plain git, I could do something like:
git rebase --onto SHA^ SHA
but there is no such method in the library.
According to pygit2 documentation, merge_trees
can be used to this end. So far, I got:
def deleteCommit(repo, commitId):
delete_commit = repo[commitId]
head = repo.head
base = repo.merge_base(delete_commit.id, head.target)
delete_commit_tree = repo.get(delete_commit).tree
head_tree = repo.get(head.target).tree
base_tree = repo.get(base).tree
index = repo.merge_trees(base_tree, head_tree, delete_commit_tree)
tree_id = index.write_tree(repo)
From what I have looked up in the documentation this is probably the most straightforward way, aside from doing some repo.walk()
stuff which I couldn't really understand that well.
I came up with this new partial solution using repo.walk()
:
def deleteCommit(repo, commitId):
# Get the branch to be rewritten
branch = repo.lookup_branch(repo.head.shorthand)
branch_ref = repo.lookup_reference(branch.name)
# Get the commit history up to the branch tip
commits = list(repo.walk(branch.target, GIT_SORT_TOPOLOGICAL))
# Create a new tree with the desired changes
tree = repo.TreeBuilder().write()
for commit in commits:
if commitId != str(commit.id):
commitOid = git.Oid(hex=commitId)
if commitOid in commit.parent_ids:
commit.parent_ids.remove(commitOid)
repo.create_commit(
branch_ref.name, # no reflog message
commit.author, # use the default signature
commit.committer, # use the default signature
commit.message, # commit message
tree, # use the new tree
commit.parent_ids,
)
else:
repo.references.delete(f'refs/commits/{commit.hex}')
# Point the branch to the new commit
branch_ref.set_target(tree)
But that yields the error pygit2.GitError: failed to create commit: current tip is not the first parent
答案1
得分: 0
I found a solution combining repo.walk()
with cherry-pick
and merge_commits
:
def removeCommitFromHistory(repo, commit_id):
repo.reset(repo.head.target, GIT_RESET_HARD)
previous_branch_ref = repo.branches[repo.head.shorthand]
previous_branch_shorthand = previous_branch_ref.shorthand
old_branch_name = f"remove_commit_{commit_id}"
previous_branch_ref.rename(old_branch_name)
old_branch_iter = repo.walk(
previous_branch_ref.target, GIT_SORT_REVERSE).__iter__()
initial_commit = next(old_branch_iter)
new_branch_ref = repo.branches.local.create(
previous_branch_shorthand, initial_commit)
repo.checkout(new_branch_ref)
remove_commit_parents = None
for commit in old_branch_iter:
if commit.hex == commit_id:
remove_commit_parents = commit.parent_ids[:]
continue
index = repo.index
if remove_commit_parents:
index = repo.merge_commits(
commit.id, remove_commit_parents[0], favor="ours")
else:
repo.cherrypick(commit.id)
parents = remove_commit_parents if str(
commit.parent_ids[0]) == commit_id else [repo.head.target]
tree_id = index.write_tree()
repo.create_commit(repo.head.name, commit.author, commit.committer,
commit.message, tree_id, parents)
repo.state_cleanup()
repo.branches[old_branch_name].delete()
Basically, I'm recreating the entire history, commit-by-commit, using cherry-pick
and using merge_commits
to merge the commit I want to delete with its parent, keeping the parent.
英文:
I found a solution combining repo.walk()
with cherry-pick
and merge_commits
:
def removeCommitFromHistory(repo, commit_id):
repo.reset(repo.head.target, GIT_RESET_HARD)
previous_branch_ref = repo.branches[repo.head.shorthand]
previous_branch_shorthand = previous_branch_ref.shorthand
old_branch_name = f"remove_commit_{commit_id}"
previous_branch_ref.rename(old_branch_name)
old_branch_iter = repo.walk(
previous_branch_ref.target, GIT_SORT_REVERSE).__iter__()
initial_commit = next(old_branch_iter)
new_branch_ref = repo.branches.local.create(
previous_branch_shorthand, initial_commit)
repo.checkout(new_branch_ref)
remove_commit_parents = None
for commit in old_branch_iter:
if commit.hex == commit_id:
remove_commit_parents = commit.parent_ids[:]
continue
index = repo.index
if remove_commit_parents:
index = repo.merge_commits(
commit.id, remove_commit_parents[0], favor="ours")
else:
repo.cherrypick(commit.id)
parents = remove_commit_parents if str(
commit.parent_ids[0]) == commit_id else [repo.head.target]
tree_id = index.write_tree()
repo.create_commit(repo.head.name, commit.author, commit.committer,
commit.message, tree_id, parents)
repo.state_cleanup()
repo.branches[old_branch_name].delete()
Basically, I'm recreating the entire history, commit-by-commit, using cherry-pick
and using merge_commits
to merge the commit I want to delete with its parent, keeping the parent.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论