使用 pygit2 如何删除特定的提交?

huangapple go评论61阅读模式
英文:

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.

huangapple
  • 本文由 发表于 2023年4月4日 09:08:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/75924772.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定