|
| 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() |
0 commit comments