Revert an overambitious search-and-replace operation.
[cvs2svn.git] / contrib / git-move-refs.py
blobc19625e2d6b16aa5688cf8d86426ebd9a5431d6f
1 #!/usr/bin/python
3 """Remove redundant fixup commits from a cvs2svn-converted git repository.
5 Process each head ref and/or tag in a git repository. If the
6 associated commit is tree-wise identical with another commit, the head
7 or tag is moved to point at the other commit (i.e., refs pointing at
8 identical content will all point at a single fixup commit).
10 Furthermore, if one of the parents of the fixup commit is identical to
11 the fixup commit itself, then the head or tag is moved to the parent.
13 The script is meant to be run against a repository converted by
14 cvs2svn, since cvs2svn creates empty commits for some tags and head
15 refs (branches).
17 """
19 usage = 'USAGE: %prog [options]'
21 import sys
22 import optparse
23 from subprocess import Popen, PIPE, call
26 # Cache trees we have already seen, and that are suitable targets for
27 # moved refs
28 tree_cache = {} # tree SHA1 -> commit SHA1
30 # Cache parent commit -> parent tree mapping
31 parent_cache = {} # commit SHA1 -> tree SHA1
34 def resolve_commit(commit):
35 """Return the tree object associated with the given commit."""
37 get_tree_cmd = ["git", "rev-parse", commit + "^{tree}"]
38 tree = Popen(get_tree_cmd, stdout = PIPE).communicate()[0].strip()
39 return tree
42 def move_ref(ref, from_commit, to_commit, ref_type):
43 """Move the given head to the given commit.
44 ref_type is either "tags" or "heads"
45 """
46 if from_commit != to_commit:
47 print "Moving ref %s from %s to %s..." % (ref, from_commit, to_commit),
48 if ref_type == "tags":
49 command = "tag"
50 else:
51 command = "branch"
52 retcode = call(["git", command, "-f", ref, to_commit])
53 if retcode == 0:
54 print "done"
55 else:
56 print "FAILED"
59 def try_to_move_ref(ref, commit, tree, parents, ref_type):
60 """Try to move the given ref to a separate commit (with identical tree)."""
62 if tree in tree_cache:
63 # We have already found a suitable commit for this tree
64 move_ref(ref, commit, tree_cache[tree], ref_type)
65 return
67 # Try to move this ref to one of its commit's parents
68 for p in parents:
69 if p not in parent_cache:
70 # Not in cache
71 parent_cache[p] = resolve_commit(p)
72 p_tree = parent_cache[p]
73 if tree == p_tree:
74 # We can move ref to parent p
75 move_ref(ref, commit, p, ref_type)
76 commit = p
77 break
79 # Register the resulting commit object in the tree_cache
80 assert tree not in tree_cache # Sanity check
81 tree_cache[tree] = commit
84 def process_refs(ref_type):
85 tree_cache.clear()
86 parent_cache.clear()
88 # Command for retrieving refs and associated metadata
89 # See 'git for-each-ref' manual page for --format details
90 get_ref_info_cmd = [
91 "git",
92 "for-each-ref",
93 "--format=%(refname)%00%(objecttype)%00%(subject)%00"
94 "%(objectname)%00%(tree)%00%(parent)%00"
95 "%(*objectname)%00%(*tree)%00%(*parent)",
96 "refs/%s" % (ref_type,),
99 get_ref_info = Popen(get_ref_info_cmd, stdout = PIPE)
101 while True: # While get_ref_info process is still running
102 for line in get_ref_info.stdout:
103 line = line.strip()
104 (ref, objtype, subject,
105 commit, tree, parents,
106 commit_alt, tree_alt, parents_alt) = line.split(chr(0))
107 if objtype == "tag":
108 commit = commit_alt
109 tree = tree_alt
110 parents = parents_alt
111 elif objtype != "commit":
112 continue
114 if subject.startswith("This commit was manufactured by cvs2svn") \
115 or not subject:
116 # We shall try to move this ref, if possible
117 parent_list = []
118 if parents:
119 parent_list = parents.split(" ")
120 for p in parent_list:
121 assert len(p) == 40
122 ref_prefix = "refs/%s/" % (ref_type,)
123 assert ref.startswith(ref_prefix)
124 try_to_move_ref(
125 ref[len(ref_prefix):], commit, tree, parent_list, ref_type
127 else:
128 # We shall not move this ref, but it is a possible target
129 # for other refs that we _do_ want to move
130 tree_cache.setdefault(tree, commit)
132 if get_ref_info.poll() is not None:
133 # Break if no longer running:
134 break
136 assert get_ref_info.returncode == 0
139 def main(args):
140 parser = optparse.OptionParser(usage=usage, description=__doc__)
141 parser.add_option(
142 '--tags', '-t',
143 action='store_true', default=False,
144 help='process tags',
146 parser.add_option(
147 '--branches', '-b',
148 action='store_true', default=False,
149 help='process branches',
152 (options, args) = parser.parse_args(args=args)
154 if args:
155 parser.error('Unexpected command-line arguments')
157 if not (options.tags or options.branches):
158 # By default, process tags but not branches:
159 options.tags = True
161 if options.tags:
162 process_refs("tags")
164 if options.branches:
165 process_refs("heads")
168 main(sys.argv[1:])