blob: 96e6896b371815d4a782b8a2b32004368fca89f2 [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
30import sys
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +000031import subprocess2
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000032
33from third_party import colorama
34from third_party.colorama import Fore, Style
35
calamity@chromium.org745ffa62014-09-08 01:03:19 +000036from git_common import current_branch, upstream, tags, get_branches_info
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +000037from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION, hash_one
borenet@google.com09156ec2015-03-26 14:10:06 +000038from git_common import run
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000039
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000040DEFAULT_SEPARATOR = ' ' * 4
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000041
42
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000043class OutputManager(object):
44 """Manages a number of OutputLines and formats them into aligned columns."""
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000045
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000046 def __init__(self):
47 self.lines = []
48 self.nocolor = False
49 self.max_column_lengths = []
50 self.num_columns = None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000051
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000052 def append(self, line):
53 # All lines must have the same number of columns.
54 if not self.num_columns:
55 self.num_columns = len(line.columns)
56 self.max_column_lengths = [0] * self.num_columns
57 assert self.num_columns == len(line.columns)
58
59 if self.nocolor:
60 line.colors = [''] * self.num_columns
61
62 self.lines.append(line)
63
64 # Update maximum column lengths.
65 for i, col in enumerate(line.columns):
66 self.max_column_lengths[i] = max(self.max_column_lengths[i], len(col))
67
68 def as_formatted_string(self):
69 return '\n'.join(
70 l.as_padded_string(self.max_column_lengths) for l in self.lines)
71
72
73class OutputLine(object):
74 """A single line of data.
75
76 This consists of an equal number of columns, colors and separators."""
77
78 def __init__(self):
79 self.columns = []
80 self.separators = []
81 self.colors = []
82
83 def append(self, data, separator=DEFAULT_SEPARATOR, color=Fore.WHITE):
84 self.columns.append(data)
85 self.separators.append(separator)
86 self.colors.append(color)
87
88 def as_padded_string(self, max_column_lengths):
89 """"Returns the data as a string with each column padded to
90 |max_column_lengths|."""
91 output_string = ''
92 for i, (color, data, separator) in enumerate(
93 zip(self.colors, self.columns, self.separators)):
94 if max_column_lengths[i] == 0:
95 continue
96
97 padding = (max_column_lengths[i] - len(data)) * ' '
98 output_string += color + data + padding + separator
99
100 return output_string.rstrip()
101
102
103class BranchMapper(object):
104 """A class which constructs output representing the tree's branch structure.
105
106 Attributes:
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000107 __branches_info: a map of branches to their BranchesInfo objects which
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000108 consist of the branch hash, upstream and ahead/behind status.
109 __gone_branches: a set of upstreams which are not fetchable by git"""
110
111 def __init__(self):
112 self.verbosity = 0
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000113 self.maxjobs = 0
borenet@google.com09156ec2015-03-26 14:10:06 +0000114 self.show_subject = False
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000115 self.output = OutputManager()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000116 self.__gone_branches = set()
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000117 self.__branches_info = None
118 self.__parent_map = collections.defaultdict(list)
119 self.__current_branch = None
120 self.__current_hash = None
121 self.__tag_set = None
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000122 self.__status_info = {}
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000123
124 def start(self):
125 self.__branches_info = get_branches_info(
126 include_tracking_status=self.verbosity >= 1)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000127 if (self.verbosity >= 2):
128 # Avoid heavy import unless necessary.
129 from git_cl import get_cl_statuses
130
131 status_info = get_cl_statuses(self.__branches_info.keys(),
132 fine_grained=self.verbosity > 2,
133 max_processes=self.maxjobs)
134
135 for _ in xrange(len(self.__branches_info)):
136 # This is a blocking get which waits for the remote CL status to be
137 # retrieved.
138 (branch, url, color) = status_info.next()
139 self.__status_info[branch] = (url, color);
140
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000141 roots = set()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000142
143 # A map of parents to a list of their children.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000144 for branch, branch_info in self.__branches_info.iteritems():
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000145 if not branch_info:
146 continue
147
148 parent = branch_info.upstream
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +0000149 if not self.__branches_info[parent]:
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000150 branch_upstream = upstream(branch)
151 # If git can't find the upstream, mark the upstream as gone.
152 if branch_upstream:
153 parent = branch_upstream
154 else:
155 self.__gone_branches.add(parent)
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000156 # A parent that isn't in the branches info is a root.
157 roots.add(parent)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000158
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000159 self.__parent_map[parent].append(branch)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000160
161 self.__current_branch = current_branch()
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000162 self.__current_hash = hash_one('HEAD', short=True)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000163 self.__tag_set = tags()
164
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000165 if roots:
166 for root in sorted(roots):
167 self.__append_branch(root)
168 else:
169 no_branches = OutputLine()
170 no_branches.append('No User Branches')
171 self.output.append(no_branches)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000172
173 def __is_invalid_parent(self, parent):
174 return not parent or parent in self.__gone_branches
175
176 def __color_for_branch(self, branch, branch_hash):
177 if branch.startswith('origin'):
178 color = Fore.RED
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +0000179 elif branch.startswith('branch-heads'):
180 color = Fore.BLUE
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000181 elif self.__is_invalid_parent(branch) or branch in self.__tag_set:
182 color = Fore.MAGENTA
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000183 elif self.__current_hash.startswith(branch_hash):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000184 color = Fore.CYAN
185 else:
186 color = Fore.GREEN
187
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +0000188 if branch_hash and self.__current_hash.startswith(branch_hash):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000189 color += Style.BRIGHT
190 else:
191 color += Style.NORMAL
192
193 return color
194
195 def __append_branch(self, branch, depth=0):
196 """Recurses through the tree structure and appends an OutputLine to the
197 OutputManager for each branch."""
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000198 branch_info = self.__branches_info[branch]
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000199 if branch_info:
200 branch_hash = branch_info.hash
201 else:
calamity@chromium.org4cd0a8b2014-09-23 03:30:50 +0000202 try:
203 branch_hash = hash_one(branch, short=True)
204 except subprocess2.CalledProcessError:
205 branch_hash = None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000206
207 line = OutputLine()
208
209 # The branch name with appropriate indentation.
210 suffix = ''
211 if branch == self.__current_branch or (
212 self.__current_branch == 'HEAD' and branch == self.__current_hash):
iannucci@chromium.orga112f032014-03-13 07:47:50 +0000213 suffix = ' *'
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000214 branch_string = branch
215 if branch in self.__gone_branches:
216 branch_string = '{%s:GONE}' % branch
217 if not branch:
218 branch_string = '{NO_UPSTREAM}'
219 main_string = ' ' * depth + branch_string + suffix
220 line.append(
221 main_string,
222 color=self.__color_for_branch(branch, branch_hash))
iannucci@chromium.orga112f032014-03-13 07:47:50 +0000223
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000224 # The branch hash.
225 if self.verbosity >= 2:
226 line.append(branch_hash or '', separator=' ', color=Fore.RED)
227
228 # The branch tracking status.
229 if self.verbosity >= 1:
230 ahead_string = ''
231 behind_string = ''
232 front_separator = ''
233 center_separator = ''
234 back_separator = ''
235 if branch_info and not self.__is_invalid_parent(branch_info.upstream):
236 ahead = branch_info.ahead
237 behind = branch_info.behind
238
239 if ahead:
240 ahead_string = 'ahead %d' % ahead
241 if behind:
242 behind_string = 'behind %d' % behind
243
244 if ahead or behind:
245 front_separator = '['
246 back_separator = ']'
247
248 if ahead and behind:
249 center_separator = '|'
250
251 line.append(front_separator, separator=' ')
252 line.append(ahead_string, separator=' ', color=Fore.MAGENTA)
253 line.append(center_separator, separator=' ')
254 line.append(behind_string, separator=' ', color=Fore.MAGENTA)
255 line.append(back_separator)
256
257 # The Rietveld issue associated with the branch.
258 if self.verbosity >= 2:
259 none_text = '' if self.__is_invalid_parent(branch) else 'None'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000260 (url, color) = self.__status_info[branch]
261 line.append(url or none_text, color=color)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000262
borenet@google.com09156ec2015-03-26 14:10:06 +0000263 # The subject of the most recent commit on the branch.
264 if self.show_subject:
265 line.append(run('log', '-n1', '--format=%s', branch))
266
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000267 self.output.append(line)
268
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000269 for child in sorted(self.__parent_map.pop(branch, ())):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000270 self.__append_branch(child, depth=depth + 1)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000271
272
273def main(argv):
274 colorama.init()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000275 if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
276 print >> sys.stderr, (
277 'This tool will not show all tracking information for git version '
278 'earlier than ' +
279 '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
280 '. Please consider upgrading.')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000281
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000282 parser = argparse.ArgumentParser(
283 description='Print a a tree of all branches parented by their upstreams')
284 parser.add_argument('-v', action='count',
285 help='Display branch hash and Rietveld URL')
286 parser.add_argument('--no-color', action='store_true', dest='nocolor',
287 help='Turn off colors.')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000288 parser.add_argument(
289 '-j', '--maxjobs', action='store', type=int,
290 help='The number of jobs to use when retrieving review status')
borenet@google.com09156ec2015-03-26 14:10:06 +0000291 parser.add_argument('--show-subject', action='store_true',
292 dest='show_subject', help='Show the commit subject.')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000293
sbc@chromium.org013731e2015-02-26 18:28:43 +0000294 opts = parser.parse_args(argv)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000295
296 mapper = BranchMapper()
297 mapper.verbosity = opts.v
298 mapper.output.nocolor = opts.nocolor
calamity@chromium.orgffde55c2015-03-12 00:44:17 +0000299 mapper.maxjobs = opts.maxjobs
borenet@google.com09156ec2015-03-26 14:10:06 +0000300 mapper.show_subject = opts.show_subject
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000301 mapper.start()
302 print mapper.output.as_formatted_string()
sbc@chromium.org013731e2015-02-26 18:28:43 +0000303 return 0
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000304
305if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000306 try:
307 sys.exit(main(sys.argv[1:]))
308 except KeyboardInterrupt:
309 sys.stderr.write('interrupted\n')
310 sys.exit(1)