3 """Remove redundant tag fixup commits from a cvs2svn-converted git repository.
5 Process each tag in a git repository. If the tagged commit is
6 tree-wise identical with another tagged commit, the tag is moved to
7 point at the other commit (i.e., tags pointing at identical content
8 will all point at a single tag fixup commit).
10 Furthermore, if one of the parents of the tag fixup commit is
11 identical to the tag fixup commit itself, then the tag is moved to the
14 The script is meant to be run against a repository converted by
15 cvs2svn, since cvs2svn creates empty commits for some tags.
19 from subprocess
import Popen
, PIPE
, call
21 # Cache trees we have already seen, and that are suitable targets for
23 tree_cache
= {} # tree SHA1 -> commit SHA1
25 # Cache parent commit -> parent tree mapping
26 parent_cache
= {} # commit SHA1 -> tree SHA1
28 def resolve_commit(commit
):
29 """Return the tree object associated with the given commit."""
31 get_tree_cmd
= ["git", "rev-parse", commit
+ "^{tree}"]
32 tree
= Popen(get_tree_cmd
, stdout
= PIPE
).communicate()[0].strip()
35 def move_tag(tag
, from_commit
, to_commit
):
36 """Move the given tag to the given commit."""
38 print "Moving tag %s from %s to %s..." % (tag
, from_commit
, to_commit
),
39 retcode
= call(["git", "tag", "-f", tag
, to_commit
])
45 def try_to_move_tag(tag
, commit
, tree
, parents
):
46 """Try to move the given tag to a separate commit (with identical tree)."""
48 if tree
in tree_cache
:
49 # We have already found a suitable commit for this tree
50 move_tag(tag
, commit
, tree_cache
[tree
])
53 # Try to move this tag to one of its commit's parents
55 if p
not in parent_cache
:
57 parent_cache
[p
] = resolve_commit(p
)
58 p_tree
= parent_cache
[p
]
60 # We can move tag to parent p
61 move_tag(tag
, commit
, p
)
65 # Register the resulting commit object in the tree_cache
66 assert tree
not in tree_cache
# Sanity check
67 tree_cache
[tree
] = commit
69 # Command for retrieving tags and associated metadata
70 # See 'git for-each-ref' manual page for --format details
74 "--format=%(refname)%00%(objecttype)%00%(subject)%00"
75 "%(objectname)%00%(tree)%00%(parent)%00"
76 "%(*objectname)%00%(*tree)%00%(*parent)",
80 get_tag_info
= Popen(get_tag_info_cmd
, stdout
= PIPE
)
82 while True: # While get_tag_info process is still running
83 for line
in get_tag_info
.stdout
:
85 (tag
, objtype
, subject
,
86 commit
, tree
, parents
,
87 commit_alt
, tree_alt
, parents_alt
) = line
.split(chr(0))
92 elif objtype
!= "commit":
95 if subject
.startswith("This commit was manufactured by cvs2svn") \
97 # We shall try to move this tag, if possible
100 parent_list
= parents
.split(" ")
101 for p
in parent_list
:
103 assert tag
.startswith("refs/tags/")
104 try_to_move_tag(tag
[10:], commit
, tree
, parent_list
)
106 # We shall not move this tag, but it is a possible target
107 # for other tags that we _do_ want to move
108 tree_cache
.setdefault(tree
, commit
)
110 if get_tag_info
.poll() is not None:
111 # Break if no longer running:
114 assert get_tag_info
.returncode
== 0