blob: 016a0330f732a9e76286c79fdea08ef4ed10ec33 [file] [log] [blame]
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +00001#!/usr/bin/env python
iannucci@chromium.orga112f032014-03-13 07:47:50 +00002# Copyright 2014 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00006"""Provides a short mapping of all the branches in your local repo, organized
7by their upstream ('tracking branch') layout.
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +00008
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00009Example:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000010origin/master
11 cool_feature
12 dependent_feature
13 other_dependent_feature
14 other_feature
15
16Branches are colorized as follows:
17 * Red - a remote branch (usually the root of all local branches)
18 * Cyan - a local branch which is the same as HEAD
19 * Note that multiple branches may be Cyan, if they are all on the same
20 commit, and you have that commit checked out.
21 * Green - a local branch
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +000022 * Blue - a 'branch-heads' branch
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000023 * Magenta - a tag
24 * Magenta '{NO UPSTREAM}' - If you have local branches which do not track any
25 upstream, then you will see this.
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000026"""
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000027
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000028import argparse
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000029import collections
iannucci@chromium.org0703ea22016-04-01 01:02:42 +000030import os
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +000031import subprocess2
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000032import sys
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000033
calamity@chromium.org745ffa62014-09-08 01:03:19 +000034from git_common import current_branch, upstream, tags, get_branches_info
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +000035from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION, hash_one
borenet@google.com09156ec2015-03-26 14:10:06 +000036from git_common import run
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000037
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000038import setup_color
39
40from third_party.colorama import Fore, Style
41
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000042DEFAULT_SEPARATOR = ' ' * 4
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000043
44
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000045class OutputManager(object):
46 """Manages a number of OutputLines and formats them into aligned columns."""
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000047
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000048 def __init__(self):
49 self.lines = []
50 self.nocolor = False
51 self.max_column_lengths = []
52 self.num_columns = None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000053
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000054 def append(self, line):
55 # All lines must have the same number of columns.
56 if not self.num_columns:
57 self.num_columns = len(line.columns)
58 self.max_column_lengths = [0] * self.num_columns
59 assert self.num_columns == len(line.columns)
60
61 if self.nocolor:
62 line.colors = [''] * self.num_columns
63
64 self.lines.append(line)
65
66 # Update maximum column lengths.
67 for i, col in enumerate(line.columns):
68 self.max_column_lengths[i] = max(self.max_column_lengths[i], len(col))
69
70 def as_formatted_string(self):
71 return '\n'.join(
72 l.as_padded_string(self.max_column_lengths) for l in self.lines)
73
74
75class OutputLine(object):
76 """A single line of data.
77
78 This consists of an equal number of columns, colors and separators."""
79
80 def __init__(self):
81 self.columns = []
82 self.separators = []
83 self.colors = []
84
85 def append(self, data, separator=DEFAULT_SEPARATOR, color=Fore.WHITE):
86 self.columns.append(data)
87 self.separators.append(separator)
88 self.colors.append(color)
89
90 def as_padded_string(self, max_column_lengths):
91 """"Returns the data as a string with each column padded to
92 |max_column_lengths|."""
93 output_string = ''
94 for i, (color, data, separator) in enumerate(
95 zip(self.colors, self.columns, self.separators)):
96 if max_column_lengths[i] == 0:
97 continue
98
99 padding = (max_column_lengths[i] - len(data)) * ' '
100 output_string += color + data + padding + separator
101
102 return output_string.rstrip()
103
104
105class BranchMapper(object):
106 """A class which constructs output representing the tree's branch structure.
107
108 Attributes:
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000109 __branches_info: a map of branches to their BranchesInfo objects which
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000110 consist of the branch hash, upstream and ahead/behind status.
111 __gone_branches: a set of upstreams which are not fetchable by git"""
112
113 def __init__(self):
114 self.verbosity = 0
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000115 self.maxjobs = 0
borenet@google.com09156ec2015-03-26 14:10:06 +0000116 self.show_subject = False
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000117 self.output = OutputManager()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000118 self.__gone_branches = set()
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000119 self.__branches_info = None
120 self.__parent_map = collections.defaultdict(list)
121 self.__current_branch = None
122 self.__current_hash = None
123 self.__tag_set = None
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000124 self.__status_info = {}
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000125
126 def start(self):
127 self.__branches_info = get_branches_info(
128 include_tracking_status=self.verbosity >= 1)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000129 if (self.verbosity >= 2):
130 # Avoid heavy import unless necessary.
sdefresne@chromium.org32f122f2015-06-11 09:39:40 +0000131 from git_cl import get_cl_statuses, color_for_status
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000132
133 status_info = get_cl_statuses(self.__branches_info.keys(),
134 fine_grained=self.verbosity > 2,
135 max_processes=self.maxjobs)
136
137 for _ in xrange(len(self.__branches_info)):
138 # This is a blocking get which waits for the remote CL status to be
139 # retrieved.
sdefresne@chromium.org32f122f2015-06-11 09:39:40 +0000140 (branch, url, status) = status_info.next()
141 self.__status_info[branch] = (url, color_for_status(status))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000142
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000143 roots = set()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000144
145 # A map of parents to a list of their children.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000146 for branch, branch_info in self.__branches_info.iteritems():
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000147 if not branch_info:
148 continue
149
150 parent = branch_info.upstream
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +0000151 if not self.__branches_info[parent]:
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000152 branch_upstream = upstream(branch)
153 # If git can't find the upstream, mark the upstream as gone.
154 if branch_upstream:
155 parent = branch_upstream
156 else:
157 self.__gone_branches.add(parent)
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000158 # A parent that isn't in the branches info is a root.
159 roots.add(parent)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000160
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000161 self.__parent_map[parent].append(branch)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000162
163 self.__current_branch = current_branch()
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000164 self.__current_hash = hash_one('HEAD', short=True)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000165 self.__tag_set = tags()
166
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000167 if roots:
168 for root in sorted(roots):
169 self.__append_branch(root)
170 else:
171 no_branches = OutputLine()
172 no_branches.append('No User Branches')
173 self.output.append(no_branches)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000174
175 def __is_invalid_parent(self, parent):
176 return not parent or parent in self.__gone_branches
177
178 def __color_for_branch(self, branch, branch_hash):
jsbell@google.com4f1fc352016-03-24 22:23:46 +0000179 if branch.startswith('origin/'):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000180 color = Fore.RED
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +0000181 elif branch.startswith('branch-heads'):
182 color = Fore.BLUE
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000183 elif self.__is_invalid_parent(branch) or branch in self.__tag_set:
184 color = Fore.MAGENTA
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000185 elif self.__current_hash.startswith(branch_hash):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000186 color = Fore.CYAN
187 else:
188 color = Fore.GREEN
189
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +0000190 if branch_hash and self.__current_hash.startswith(branch_hash):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000191 color += Style.BRIGHT
192 else:
193 color += Style.NORMAL
194
195 return color
196
197 def __append_branch(self, branch, depth=0):
198 """Recurses through the tree structure and appends an OutputLine to the
199 OutputManager for each branch."""
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000200 branch_info = self.__branches_info[branch]
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000201 if branch_info:
202 branch_hash = branch_info.hash
203 else:
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +0000204 try:
205 branch_hash = hash_one(branch, short=True)
206 except subprocess2.CalledProcessError:
207 branch_hash = None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000208
209 line = OutputLine()
210
211 # The branch name with appropriate indentation.
212 suffix = ''
213 if branch == self.__current_branch or (
214 self.__current_branch == 'HEAD' and branch == self.__current_hash):
iannucci@chromium.orga112f032014-03-13 07:47:50 +0000215 suffix = ' *'
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000216 branch_string = branch
217 if branch in self.__gone_branches:
218 branch_string = '{%s:GONE}' % branch
219 if not branch:
220 branch_string = '{NO_UPSTREAM}'
221 main_string = ' ' * depth + branch_string + suffix
222 line.append(
223 main_string,
224 color=self.__color_for_branch(branch, branch_hash))
iannucci@chromium.orga112f032014-03-13 07:47:50 +0000225
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000226 # The branch hash.
227 if self.verbosity >= 2:
228 line.append(branch_hash or '', separator=' ', color=Fore.RED)
229
230 # The branch tracking status.
231 if self.verbosity >= 1:
232 ahead_string = ''
233 behind_string = ''
234 front_separator = ''
235 center_separator = ''
236 back_separator = ''
237 if branch_info and not self.__is_invalid_parent(branch_info.upstream):
238 ahead = branch_info.ahead
239 behind = branch_info.behind
240
241 if ahead:
242 ahead_string = 'ahead %d' % ahead
243 if behind:
244 behind_string = 'behind %d' % behind
245
246 if ahead or behind:
247 front_separator = '['
248 back_separator = ']'
249
250 if ahead and behind:
251 center_separator = '|'
252
253 line.append(front_separator, separator=' ')
254 line.append(ahead_string, separator=' ', color=Fore.MAGENTA)
255 line.append(center_separator, separator=' ')
256 line.append(behind_string, separator=' ', color=Fore.MAGENTA)
257 line.append(back_separator)
258
259 # The Rietveld issue associated with the branch.
260 if self.verbosity >= 2:
261 none_text = '' if self.__is_invalid_parent(branch) else 'None'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000262 (url, color) = self.__status_info[branch]
263 line.append(url or none_text, color=color)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000264
borenet@google.com09156ec2015-03-26 14:10:06 +0000265 # The subject of the most recent commit on the branch.
266 if self.show_subject:
267 line.append(run('log', '-n1', '--format=%s', branch))
268
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000269 self.output.append(line)
270
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000271 for child in sorted(self.__parent_map.pop(branch, ())):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000272 self.__append_branch(child, depth=depth + 1)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000273
274
275def main(argv):
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000276 setup_color.init()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000277 if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
278 print >> sys.stderr, (
279 'This tool will not show all tracking information for git version '
280 'earlier than ' +
281 '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
282 '. Please consider upgrading.')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000283
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000284 parser = argparse.ArgumentParser(
285 description='Print a a tree of all branches parented by their upstreams')
286 parser.add_argument('-v', action='count',
287 help='Display branch hash and Rietveld URL')
288 parser.add_argument('--no-color', action='store_true', dest='nocolor',
289 help='Turn off colors.')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000290 parser.add_argument(
291 '-j', '--maxjobs', action='store', type=int,
292 help='The number of jobs to use when retrieving review status')
borenet@google.com09156ec2015-03-26 14:10:06 +0000293 parser.add_argument('--show-subject', action='store_true',
294 dest='show_subject', help='Show the commit subject.')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000295
sbc@chromium.org013731e2015-02-26 18:28:43 +0000296 opts = parser.parse_args(argv)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000297
298 mapper = BranchMapper()
299 mapper.verbosity = opts.v
300 mapper.output.nocolor = opts.nocolor
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000301 mapper.maxjobs = opts.maxjobs
borenet@google.com09156ec2015-03-26 14:10:06 +0000302 mapper.show_subject = opts.show_subject
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000303 mapper.start()
304 print mapper.output.as_formatted_string()
sbc@chromium.org013731e2015-02-26 18:28:43 +0000305 return 0
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000306
307if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000308 try:
309 sys.exit(main(sys.argv[1:]))
310 except KeyboardInterrupt:
311 sys.stderr.write('interrupted\n')
312 sys.exit(1)