Skip to content

Commit 1afdd50

Browse files
committed
First attempt at git-explode
1 parent 19c2aa0 commit 1afdd50

10 files changed

Lines changed: 381 additions & 138 deletions

File tree

git_explode/cli.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
This is a skeleton file that can serve as a starting point for a Python
6+
console script. To run this script uncomment the following line in the
7+
entry_points section in setup.cfg:
8+
9+
console_scripts =
10+
fibonacci = git_explode.skeleton:run
11+
12+
Then run `python setup.py install` which will install the command `fibonacci`
13+
inside your current environment.
14+
Besides console scripts, the header (i.e. until _logger...) of this file can
15+
also be used as template for Python modules.
16+
17+
Note: This skeleton file can be safely removed if not needed!
18+
"""
19+
20+
from __future__ import print_function, absolute_import
21+
22+
import argparse
23+
import copy
24+
import sys
25+
import logging
26+
27+
from ostruct import OpenStruct
28+
29+
from git_deps.detector import DependencyDetector
30+
from git_deps.gitutils import GitUtils
31+
from git_deps.utils import abort, standard_logger
32+
from git_explode import __version__
33+
from git_explode.gitutils import GitUtils as GitExplodeUtils
34+
from git_explode.listener import ExplodeDependencyListener
35+
from git_explode.topics import TopicManager
36+
37+
__author__ = "Adam Spiers"
38+
__copyright__ = "Adam Spiers"
39+
__license__ = "GPL-2+"
40+
41+
42+
def get_dependencies(repo, args):
43+
"""
44+
Detect commit dependency tree, and return a tuple of dicts mapping
45+
this in both directions. Note that the dependency tree goes in
46+
the reverse direction to the git commit graph, in that the leaves
47+
of the dependency tree are the oldest commits, because newer
48+
commits depend on older commits
49+
50+
:param args: results from parse_args
51+
:return: (dependencies_from, dependencies_on)
52+
"""
53+
54+
detector_args = OpenStruct({
55+
'recurse': True,
56+
'exclude_commits': [args.base],
57+
'debug': args.debug,
58+
'context_lines': args.context_lines,
59+
})
60+
detector = DependencyDetector(detector_args, repo)
61+
listener = ExplodeDependencyListener(args)
62+
detector.add_listener(listener)
63+
64+
revs = GitUtils.rev_list("%s..%s" % (args.base, args.head))
65+
for rev in revs:
66+
try:
67+
detector.find_dependencies(rev)
68+
except KeyboardInterrupt:
69+
pass
70+
71+
return (detector.commits,
72+
listener.dependencies_from(),
73+
listener.dependencies_on())
74+
75+
76+
def explode(logger, base, commits, deps_from, deps_on):
77+
"""
78+
Walk the dependency tree breadth-first starting with the
79+
leaves at the bottom.
80+
81+
For each commit, figure out whether it should be exploded
82+
83+
:param base: sha on which to base all exploded topics
84+
:param deps_from: dict mapping dependents to dependencies
85+
:param deps_on: dict mapping in opposite direction
86+
"""
87+
todo = get_leaves(commits, deps_from)
88+
89+
# Each time we explode a commit, we'll remove it from any
90+
# dict which is a value of this dict.
91+
unexploded_deps_from = copy.deepcopy(deps_from)
92+
93+
logger.debug("queue:")
94+
for commit in todo:
95+
logger.debug(' ' + GitUtils.commit_summary(commit))
96+
97+
current = None
98+
topic_mgr = TopicManager('topic%d')
99+
100+
while todo:
101+
commit = todo.pop(0)
102+
sha = commit.hex
103+
if unexploded_deps_from[sha]:
104+
abort("BUG: unexploded deps from %s" %
105+
GitUtils.commit_summary(commit))
106+
107+
deps = deps_from[sha]
108+
109+
if not deps:
110+
base_desc = GitUtils.commit_summary(base)
111+
branch = topic_mgr.register(sha)
112+
print("checkout %s on base %s" % (branch, base_desc))
113+
current = branch
114+
else:
115+
deps = deps.keys()
116+
logger.debug("deps: %r" % deps)
117+
existing_branch = topic_mgr.lookup(*deps)
118+
if len(deps) == 1:
119+
assert existing_branch is not None
120+
branch = existing_branch
121+
topic_mgr.assign(branch, sha)
122+
if current != branch:
123+
print("checkout %s" % branch)
124+
current = branch
125+
else:
126+
if existing_branch is None:
127+
branch = topic_mgr.register(*deps)
128+
base_desc = GitUtils.commit_summary(commits[deps[0]])
129+
print("checkout %s on %s" % (branch, base_desc))
130+
current = branch
131+
to_merge = deps[1:]
132+
print("merge %s" % ' '.join(to_merge))
133+
else:
134+
# Can reuse existing merge commit, but
135+
# create a new branch at the same point
136+
branch = topic_mgr.register(*deps)
137+
print("checkout -b %s %s" % (branch, existing_branch))
138+
139+
print("cherrypick %s" % GitUtils.commit_summary(commit))
140+
141+
for dependent in deps_on[commit.hex]:
142+
del unexploded_deps_from[dependent][commit.hex]
143+
if not unexploded_deps_from[dependent]:
144+
new = commits[dependent]
145+
logger.debug("pushed to queue: %s" %
146+
GitUtils.commit_summary(new))
147+
todo.insert(0, new)
148+
149+
150+
def get_leaves(commits, deps_from):
151+
"""
152+
Return all the leaves of the dependency tree, i.e. commits with
153+
no child dependencies
154+
"""
155+
leaves = []
156+
for sha, dependencies in deps_from.iteritems():
157+
if len(dependencies) == 0:
158+
leaves.append(commits[sha])
159+
return leaves
160+
161+
162+
def parse_args(args):
163+
"""
164+
Parse command line parameters
165+
166+
:param args: command line parameters as list of strings
167+
:return: command line parameters as :obj:`argparse.Namespace`
168+
"""
169+
parser = argparse.ArgumentParser(
170+
description="Explode linear sequence of commits into topic branches")
171+
parser.add_argument(
172+
'--version',
173+
action='version',
174+
version='git-explode {ver}'.format(ver=__version__))
175+
parser.add_argument(
176+
'-v', '--verbose',
177+
dest="loglevel",
178+
help="set loglevel to INFO",
179+
action='store_const',
180+
const=logging.INFO)
181+
parser.add_argument(
182+
'-d', '--debug',
183+
dest='debug',
184+
action='store_true',
185+
help='Show debugging')
186+
parser.add_argument(
187+
'-p', '--prefix',
188+
dest="prefix",
189+
help="prefix for all created topic branches",
190+
type=str,
191+
metavar="BASE")
192+
parser.add_argument(
193+
'-c', '--context-lines',
194+
dest='context_lines',
195+
help='Number of lines of diff context to use [%(default)s]',
196+
type=int,
197+
metavar='NUM',
198+
default=1)
199+
parser.add_argument(
200+
dest="base",
201+
help="base of sequence to explode",
202+
type=str,
203+
metavar="BASE")
204+
parser.add_argument(
205+
dest="head",
206+
help="head of sequence to explode",
207+
type=str)
208+
209+
return parser.parse_args(args)
210+
211+
212+
def main(args):
213+
args = parse_args(args)
214+
logger = standard_logger('git-explode', args.debug)
215+
216+
repo = GitUtils.get_repo()
217+
commits, deps_from, deps_on = get_dependencies(repo, args)
218+
base = GitUtils.ref_commit(repo, args.base)
219+
logger.debug("base commit %s is %s" %
220+
(args.base, GitUtils.commit_summary(base)))
221+
orig_head = GitExplodeUtils.get_head()
222+
explode(logger, base, commits, deps_from, deps_on)
223+
print("checkout %s" % orig_head)
224+
225+
226+
def run():
227+
main(sys.argv[1:])
228+
229+
230+
if __name__ == "__main__":
231+
run()

git_explode/gitutils.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
This is a skeleton file that can serve as a starting point for a Python
6+
console script. To run this script uncomment the following line in the
7+
entry_points section in setup.cfg:
8+
9+
console_scripts =
10+
fibonacci = git_explode.skeleton:run
11+
12+
Then run `python setup.py install` which will install the command `fibonacci`
13+
inside your current environment.
14+
Besides console scripts, the header (i.e. until _logger...) of this file can
15+
also be used as template for Python modules.
16+
17+
Note: This skeleton file can be safely removed if not needed!
18+
"""
19+
20+
from __future__ import print_function, absolute_import
21+
22+
import subprocess
23+
24+
25+
class GitUtils(object):
26+
@classmethod
27+
def get_head(cls):
28+
"""
29+
"""
30+
try:
31+
return subprocess.check_output(['git', 'symbolic-ref',
32+
'--short', '-q', 'HEAD']).rstrip()
33+
except subprocess.CalledProcessError:
34+
return subprocess.check_output(['git', 'rev-parse',
35+
'HEAD']).rstrip()

git_explode/listener.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from git_deps.listener.base import DependencyListener
2+
3+
4+
class ExplodeDependencyListener(DependencyListener):
5+
"""Dependency listener for use when building a dependency tree to be
6+
used for exploding the commits into multiple topic branches.
7+
"""
8+
9+
def __init__(self, options):
10+
super(ExplodeDependencyListener, self).__init__(options)
11+
12+
# Map each commit to a dict whose keys are the dependencies of
13+
# that commit which haven't yet been exploded into a topic
14+
# branch.
15+
self._dependencies_from = {}
16+
self._dependencies_on = {}
17+
18+
def new_commit(self, commit):
19+
"""Adds the commit if it doesn't already exist.
20+
"""
21+
sha1 = commit.hex
22+
for d in (self._dependencies_from, self._dependencies_on):
23+
if sha1 not in d:
24+
d[sha1] = {}
25+
26+
def new_dependency(self, dependee, dependency, path, line_num):
27+
src = dependee.hex
28+
dst = dependency.hex
29+
30+
cause = "%s:%d" % (path, line_num)
31+
self._dependencies_from[src][dst] = cause
32+
self._dependencies_on[dst][src] = cause
33+
34+
def dependencies_from(self):
35+
return self._dependencies_from
36+
37+
def dependencies_on(self):
38+
return self._dependencies_on

0 commit comments

Comments
 (0)