-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathexploder.py
203 lines (170 loc) · 7.43 KB
/
exploder.py
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import copy
import six
from ostruct import OpenStruct
from git_deps.detector import DependencyDetector
from git_deps.gitutils import GitUtils
from git_deps.utils import abort, standard_logger
from git_explode.gitutils import GitUtils as GitExplodeUtils
from git_explode.listener import ExplodeDependencyListener
from git_explode.topics import TopicManager
class GitExploder(object):
"""Explode a linear sequence of git commits into multiple independent
topic branches.
"""
def __init__(self, repo, base, head, debug, context_lines):
self.logger = standard_logger('git-explode', debug)
self.debug = debug
self.repo = repo
self.base = base
self.base_commit = GitUtils.ref_commit(repo, base)
self.logger.debug("base commit %s is %s" %
(base, GitUtils.commit_summary(self.base_commit)))
self.head = head
self.context_lines = context_lines
self.topic_mgr = TopicManager('topic%d', self.logger)
# Map commits to their exploded version
self.exploded = {}
def run(self):
orig_head = GitExplodeUtils.get_head()
commits, deps_from, deps_on = self.get_dependencies()
self.explode(commits, deps_from, deps_on)
self.checkout(orig_head)
def get_dependencies(self):
"""
Detect commit dependency tree, and return a tuple of dicts mapping
this in both directions. Note that the dependency tree goes in
the reverse direction to the git commit graph, in that the leaves
of the dependency tree are the oldest commits, because newer
commits depend on older commits
:return: (dependencies_from, dependencies_on)
"""
detector_args = OpenStruct({
'recurse': True,
'exclude_commits': [self.base],
'debug': self.debug,
'context_lines': self.context_lines,
})
detector = DependencyDetector(detector_args, self.repo)
listener = ExplodeDependencyListener({})
detector.add_listener(listener)
revs = GitUtils.rev_list("%s..%s" % (self.base, self.head))
for rev in revs:
try:
detector.find_dependencies(rev)
except KeyboardInterrupt:
pass
return (detector.commits,
listener.dependencies_from(),
listener.dependencies_on())
def explode(self, commits, deps_from, deps_on):
"""
Walk the dependency tree breadth-first starting with the
leaves at the bottom.
For each commit, figure out whether it should be exploded
:param commits: dict mapping SHA1 hashes to pygit2.Commit objects
:param deps_from: dict mapping dependents to dependencies
:param deps_on: dict mapping in opposite direction
"""
todo = self.get_leaves(commits, deps_from)
# Each time we explode a commit, we'll remove it from any
# dict which is a value of this dict.
unexploded_deps_from = copy.deepcopy(deps_from)
self.logger.debug("Initial queue of leaves:")
for commit in todo:
self.logger.debug(' ' + GitUtils.commit_summary(commit))
self.current_branch = None
while todo:
commit = todo.pop(0)
sha = commit.hex
self.logger.debug("Exploding %s" % GitUtils.commit_summary(commit))
if unexploded_deps_from[sha]:
abort("BUG: unexploded deps from %s" %
GitUtils.commit_summary(commit))
deps = deps_from[sha]
self.prepare_cherrypick_base(sha, deps, commits)
self.cherry_pick(sha)
self.queue_new_leaves(todo, commit, commits, deps_on,
unexploded_deps_from)
def prepare_cherrypick_base(self, sha, deps, commits):
if not deps:
branch = self.topic_mgr.next()
# We don't assign the topic here, because it will get
# assigned by cherry_pick(), and it needs to be done there
# to also catch the case where we are cherry-picking to
# update an existing branch.
self.checkout_new(branch, self.base)
return
deps = deps.keys()
assert len(deps) >= 1
self.logger.debug(" deps: %s" % ' '.join([d[:8] for d in deps]))
existing_branch = self.topic_mgr.lookup(*deps)
if len(deps) == 1:
if existing_branch is None:
self.checkout_new_dependent_topic(deps)
else:
branch = existing_branch
self.checkout(branch)
elif len(deps) > 1:
# We'll need to base the cherry-pick on a merge commit
if existing_branch is None:
self.checkout_new_dependent_topic(deps)
to_merge = (self.exploded[dep] for dep in deps[1:])
GitExplodeUtils.git('merge', *to_merge)
else:
# Can reuse existing merge commit, but
# create a new branch at the same point
self.checkout_new(branch, existing_branch)
def queue_new_leaves(self, todo, exploded_commit, commits, deps_on,
unexploded_deps_from):
"""When a commit is exploded, there may be other commits in the
dependency tree which only had a single dependency on this
commit. In that case they have effectively become leaves on
the dependency tree of unexploded commits, so they should be
added to the explode queue.
"""
sha1 = exploded_commit.hex
for dependent in deps_on[sha1]:
del unexploded_deps_from[dependent][sha1]
if not unexploded_deps_from[dependent]:
new = commits[dependent]
self.logger.debug("+ pushed to queue: %s" %
GitUtils.commit_summary(new))
todo.insert(0, new)
def get_leaves(self, commits, deps_from):
"""
Return all the leaves of the dependency tree, i.e. commits with
no child dependencies
"""
leaves = []
for sha, dependencies in six.iteritems(deps_from):
if len(dependencies) == 0:
leaves.append(commits[sha])
return leaves
def checkout(self, branch):
if self.current_branch == branch:
return
# self.logger.debug("checkout %s" % branch)
GitExplodeUtils.checkout(branch)
self.current_branch = branch
def checkout_new(self, branch, at):
assert self.current_branch != branch
# self.logger.debug("checkout -b %s %s" % (branch, at))
GitExplodeUtils.checkout_new(branch, at)
self.current_branch = branch
def checkout_new_dependent_topic(self, deps):
branch = self.topic_mgr.register(*deps)
base = self.exploded[deps[0]]
self.checkout_new(branch, base)
def cherry_pick(self, sha):
GitExplodeUtils.git('cherry-pick', sha)
self.update_current_topic(sha)
head = GitExplodeUtils.get_head_sha1()
self.exploded[sha] = head
commit = GitUtils.ref_commit(self.repo, sha)
self.logger.debug("- cherry-picked %s as %s (%s)" %
(sha[:8], self.exploded[sha][:8],
GitUtils.oneline(commit)))
def update_current_topic(self, *commits):
self.topic_mgr.assign(self.current_branch, *commits)