File: git-move-refs.py

package info (click to toggle)
cvs2svn 2.4.0-4
  • links: PTS
  • area: main
  • in suites: stretch
  • size: 3,720 kB
  • sloc: python: 22,383; sh: 512; perl: 121; makefile: 84
file content (170 lines) | stat: -rwxr-xr-x 5,284 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/python

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

Process each head ref and/or tag in a git repository. If the
associated commit is tree-wise identical with another commit, the head
or tag is moved to point at the other commit (i.e., refs pointing at
identical content will all point at a single fixup commit).

Furthermore, if one of the parents of the fixup commit is identical to
the fixup commit itself, then the head or tag 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 tags and head
refs (branches).

"""

usage = 'USAGE: %prog [options]'

import sys
import optparse
from subprocess import Popen, PIPE, call


# Cache trees we have already seen, and that are suitable targets for
# moved refs
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_ref(ref, from_commit, to_commit, ref_type):
    """Move the given head to the given commit.
    ref_type is either "tags" or "heads"
    """
    if from_commit != to_commit:
        print "Moving ref %s from %s to %s..." % (ref, from_commit, to_commit),
        if ref_type == "tags":
            command = "tag"
        else:
            command = "branch"
        retcode = call(["git", command, "-f", ref, to_commit])
        if retcode == 0:
            print "done"
        else:
            print "FAILED"


def try_to_move_ref(ref, commit, tree, parents, ref_type):
    """Try to move the given ref to a separate commit (with identical tree)."""

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

    # Try to move this ref 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 ref to parent p
            move_ref(ref, commit, p, ref_type)
            commit = p
            break

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


def process_refs(ref_type):
    tree_cache.clear()
    parent_cache.clear()

    # Command for retrieving refs and associated metadata
    # See 'git for-each-ref' manual page for --format details
    get_ref_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/%s" % (ref_type,),
    ]

    get_ref_info = Popen(get_ref_info_cmd, stdout = PIPE)

    while True: # While get_ref_info process is still running
        for line in get_ref_info.stdout:
            line = line.strip()
            (ref, 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 ref, if possible
                parent_list = []
                if parents:
                    parent_list = parents.split(" ")
                for p in parent_list:
                    assert len(p) == 40
                ref_prefix = "refs/%s/" % (ref_type,)
                assert ref.startswith(ref_prefix)
                try_to_move_ref(
                    ref[len(ref_prefix):], commit, tree, parent_list, ref_type
                    )
            else:
                # We shall not move this ref, but it is a possible target
                # for other refs that we _do_ want to move
                tree_cache.setdefault(tree, commit)

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

    assert get_ref_info.returncode == 0


def main(args):
    parser = optparse.OptionParser(usage=usage, description=__doc__)
    parser.add_option(
        '--tags', '-t',
        action='store_true', default=False,
        help='process tags',
        )
    parser.add_option(
        '--branches', '-b',
        action='store_true', default=False,
        help='process branches',
        )

    (options, args) = parser.parse_args(args=args)

    if args:
        parser.error('Unexpected command-line arguments')

    if not (options.tags or options.branches):
        # By default, process tags but not branches:
        options.tags = True

    if options.tags:
        process_refs("tags")

    if options.branches:
        process_refs("heads")


main(sys.argv[1:])