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