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
19 usage
= 'USAGE: %prog [options]'
23 from subprocess
import Popen
, PIPE
, call
26 # Cache trees we have already seen, and that are suitable targets for
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()
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"
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":
52 retcode
= call(["git", command
, "-f", ref
, to_commit
])
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
)
67 # Try to move this ref to one of its commit's parents
69 if p
not in parent_cache
:
71 parent_cache
[p
] = resolve_commit(p
)
72 p_tree
= parent_cache
[p
]
74 # We can move ref to parent p
75 move_ref(ref
, commit
, p
, ref_type
)
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
):
88 # Command for retrieving refs and associated metadata
89 # See 'git for-each-ref' manual page for --format details
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
:
104 (ref
, objtype
, subject
,
105 commit
, tree
, parents
,
106 commit_alt
, tree_alt
, parents_alt
) = line
.split(chr(0))
110 parents
= parents_alt
111 elif objtype
!= "commit":
114 if subject
.startswith("This commit was manufactured by cvs2svn") \
116 # We shall try to move this ref, if possible
119 parent_list
= parents
.split(" ")
120 for p
in parent_list
:
122 ref_prefix
= "refs/%s/" % (ref_type
,)
123 assert ref
.startswith(ref_prefix
)
125 ref
[len(ref_prefix
):], commit
, tree
, parent_list
, ref_type
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:
136 assert get_ref_info
.returncode
== 0
140 parser
= optparse
.OptionParser(usage
=usage
, description
=__doc__
)
143 action
='store_true', default
=False,
148 action
='store_true', default
=False,
149 help='process branches',
152 (options
, args
) = parser
.parse_args(args
=args
)
155 parser
.error('Unexpected command-line arguments')
157 if not (options
.tags
or options
.branches
):
158 # By default, process tags but not branches:
165 process_refs("branches")