contribution: git-move-heads.py

Steve Folly <[email protected]>
Newsgroups gmane.comp.version-control.subversion.cvs2svn.user
Message-ID <[email protected]>
I hope you find this useful - I took the git-move-tags.py script and modified it to do the same thing for branches (heads).

I have many branches in my CVS repository, and because I'm now splitting it up into multiple git repositories, sometimes there are branches with no commits on them. I realised I could take the git-move-tags.py script and apply the same principles to heads.  I know moving heads isn't so important as moving tags, since you'll naturally get a branch in tree on it's first commit anyway, but nevertheless it makes the commit tree more aesthetically pleasing to look at :-)

I wasn't sure to whether to munge them together or not, so here's a separate git-move-heads.py to start off with.

------------------------------------------------------
http://cvs2svn.tigris.org/ds/viewMessage.do?dsForumId=1670&dsMessageId=2447471

To unsubscribe from this discussion, e-mail: [[email protected]].




Regards,
Steve
git-move-heads.py (text/x-python-script, 3.8 KB)
#!/usr/bin/python

"""Remove redundant head fixup commits from a cvs2svn-converted git repository.

Process each head in a git repository. If the referenced commit is
tree-wise identical with another commit, the head is moved to
point at the other commit (i.e., heads pointing at identical content
will all point at a single fixup commit).

Furthermore, if one of the parents of the branch fixup commit is
identical to the branch fixup commit itself, then the branch is moved to the
parent.

The script is meant to be run against a repository converted by
cvs2svn, since cvs2svn creates empty commits for some branches.

"""

from subprocess import Popen, PIPE, call

# Cache trees we have already seen, and that are suitable targets for
# moved heads
tree_cache = {} # tree SHA1 -> commit SHA1

# Cache parent commit -> parent tree mapping
parent_cache = {} # commit SHA1 -> tree SHA1

def resolve_commit(commit):
    """Return the tree object associated with the given commit."""

    get_tree_cmd = ["git", "rev-parse", commit + "^{tree}"]
    tree = Popen(get_tree_cmd, stdout = PIPE).communicate()[0].strip()
    return tree

def move_head(head, from_commit, to_commit):
    """Move the given head to the given commit."""

    if from_commit != to_commit:
        print "Moving head %s from %s to %s..." % (head, from_commit, to_commit),
        retcode = call(["git", "branch", "-f", head, to_commit])
        if retcode == 0:
            print "done"
        else:
            print "FAILED"

def try_to_move_head(head, commit, tree, parents):
    """Try to move the given head to a separate commit (with identical tree)."""

    if tree in tree_cache:
        # We have already found a suitable commit for this tree
        move_head(head, commit, tree_cache[tree])
        return

    # Try to move this head to one of its commit's parents
    for p in parents:
        if p not in parent_cache:
            # Not in cache
            parent_cache[p] = resolve_commit(p)
        p_tree = parent_cache[p]
        if tree == p_tree:
            # We can move head to parent p
            move_head(head, commit, p)
            commit = p
            break

    # Register the resulting commit object in the tree_cache
    assert tree not in tree_cache # Sanity check
    tree_cache[tree] = commit

# Command for retrieving heads and associated metadata
# See 'git for-each-ref' manual page for --format details
get_head_info_cmd = [
    "git",
    "for-each-ref",
    "--format=%(refname)%00%(objecttype)%00%(subject)%00"
              "%(objectname)%00%(tree)%00%(parent)%00"
              "%(*objectname)%00%(*tree)%00%(*parent)",
    "refs/heads",
]

get_head_info = Popen(get_head_info_cmd, stdout = PIPE)

while True: # While get_head_info process is still running
    for line in get_head_info.stdout:
        line = line.strip()
        (head, objtype, subject,
         commit, tree, parents,
         commit_alt, tree_alt, parents_alt) = line.split(chr(0))
        if objtype == "tag":
            commit = commit_alt
            tree = tree_alt
            parents = parents_alt
        elif objtype != "commit":
            continue

        if subject.startswith("This commit was manufactured by cvs2svn") \
               or not subject:
            # We shall try to move this head, if possible
            parent_list = []
            if parents:
                parent_list = parents.split(" ")
            for p in parent_list:
                assert len(p) == 40
            assert head.startswith("refs/heads/")
            try_to_move_head(head[11:], commit, tree, parent_list)
        else:
            # We shall not move this head, but it is a possible target
            # for other heads that we _do_ want to move
            tree_cache.setdefault(tree, commit)

    if get_head_info.poll() is not None:
        # Break if no longer running:
        break

assert get_head_info.returncode == 0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.