blob: 96e6896b371815d4a782b8a2b32004368fca89f2 [file] [log] [blame]
[email protected]8bc9b5c2014-03-12 01:36:181#!/usr/bin/env python
[email protected]a112f032014-03-13 07:47:502# 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
[email protected]9d2c8802014-09-03 02:04:466"""Provides a short mapping of all the branches in your local repo, organized
7by their upstream ('tracking branch') layout.
[email protected]8bc9b5c2014-03-12 01:36:188
[email protected]9d2c8802014-09-03 02:04:469Example:
[email protected]8bc9b5c2014-03-12 01:36:1810origin/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
[email protected]4cd0a8b2014-09-23 03:30:5022 * Blue - a 'branch-heads' branch
[email protected]c050a5b2014-03-26 06:18:5023 * Magenta - a tag
24 * Magenta '{NO UPSTREAM}' - If you have local branches which do not track any
25 upstream, then you will see this.
[email protected]8bc9b5c2014-03-12 01:36:1826"""
[email protected]c050a5b2014-03-26 06:18:5027
[email protected]9d2c8802014-09-03 02:04:4628import argparse
[email protected]8bc9b5c2014-03-12 01:36:1829import collections
30import sys
[email protected]4cd0a8b2014-09-23 03:30:5031import subprocess2
[email protected]8bc9b5c2014-03-12 01:36:1832
33from third_party import colorama
34from third_party.colorama import Fore, Style
35
[email protected]745ffa62014-09-08 01:03:1936from git_common import current_branch, upstream, tags, get_branches_info
[email protected]4c82eb52014-09-08 02:12:2437from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION, hash_one
[email protected]09156ec2015-03-26 14:10:0638from git_common import run
[email protected]8bc9b5c2014-03-12 01:36:1839
[email protected]9d2c8802014-09-03 02:04:4640DEFAULT_SEPARATOR = ' ' * 4
[email protected]c050a5b2014-03-26 06:18:5041
42
[email protected]9d2c8802014-09-03 02:04:4643class OutputManager(object):
44 """Manages a number of OutputLines and formats them into aligned columns."""
[email protected]c050a5b2014-03-26 06:18:5045
[email protected]9d2c8802014-09-03 02:04:4646 def __init__(self):
47 self.lines = []
48 self.nocolor = False
49 self.max_column_lengths = []
50 self.num_columns = None
[email protected]c050a5b2014-03-26 06:18:5051
[email protected]9d2c8802014-09-03 02:04:4652 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:
[email protected]745ffa62014-09-08 01:03:19107 __branches_info: a map of branches to their BranchesInfo objects which
[email protected]9d2c8802014-09-03 02:04:46108 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
[email protected]ffde55c2015-03-12 00:44:17113 self.maxjobs = 0
[email protected]09156ec2015-03-26 14:10:06114 self.show_subject = False
[email protected]9d2c8802014-09-03 02:04:46115 self.output = OutputManager()
[email protected]9d2c8802014-09-03 02:04:46116 self.__gone_branches = set()
[email protected]745ffa62014-09-08 01:03:19117 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
[email protected]ffde55c2015-03-12 00:44:17122 self.__status_info = {}
[email protected]745ffa62014-09-08 01:03:19123
124 def start(self):
125 self.__branches_info = get_branches_info(
126 include_tracking_status=self.verbosity >= 1)
[email protected]ffde55c2015-03-12 00:44:17127 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
[email protected]745ffa62014-09-08 01:03:19141 roots = set()
[email protected]9d2c8802014-09-03 02:04:46142
143 # A map of parents to a list of their children.
[email protected]745ffa62014-09-08 01:03:19144 for branch, branch_info in self.__branches_info.iteritems():
[email protected]9d2c8802014-09-03 02:04:46145 if not branch_info:
146 continue
147
148 parent = branch_info.upstream
[email protected]4cd0a8b2014-09-23 03:30:50149 if not self.__branches_info[parent]:
[email protected]9d2c8802014-09-03 02:04:46150 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)
[email protected]745ffa62014-09-08 01:03:19156 # A parent that isn't in the branches info is a root.
157 roots.add(parent)
[email protected]9d2c8802014-09-03 02:04:46158
[email protected]745ffa62014-09-08 01:03:19159 self.__parent_map[parent].append(branch)
[email protected]9d2c8802014-09-03 02:04:46160
161 self.__current_branch = current_branch()
[email protected]4c82eb52014-09-08 02:12:24162 self.__current_hash = hash_one('HEAD', short=True)
[email protected]9d2c8802014-09-03 02:04:46163 self.__tag_set = tags()
164
[email protected]4c82eb52014-09-08 02:12:24165 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)
[email protected]9d2c8802014-09-03 02:04:46172
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
[email protected]4cd0a8b2014-09-23 03:30:50179 elif branch.startswith('branch-heads'):
180 color = Fore.BLUE
[email protected]9d2c8802014-09-03 02:04:46181 elif self.__is_invalid_parent(branch) or branch in self.__tag_set:
182 color = Fore.MAGENTA
[email protected]4c82eb52014-09-08 02:12:24183 elif self.__current_hash.startswith(branch_hash):
[email protected]9d2c8802014-09-03 02:04:46184 color = Fore.CYAN
185 else:
186 color = Fore.GREEN
187
[email protected]4cd0a8b2014-09-23 03:30:50188 if branch_hash and self.__current_hash.startswith(branch_hash):
[email protected]9d2c8802014-09-03 02:04:46189 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."""
[email protected]745ffa62014-09-08 01:03:19198 branch_info = self.__branches_info[branch]
[email protected]4c82eb52014-09-08 02:12:24199 if branch_info:
200 branch_hash = branch_info.hash
201 else:
[email protected]4cd0a8b2014-09-23 03:30:50202 try:
203 branch_hash = hash_one(branch, short=True)
204 except subprocess2.CalledProcessError:
205 branch_hash = None
[email protected]9d2c8802014-09-03 02:04:46206
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):
[email protected]a112f032014-03-13 07:47:50213 suffix = ' *'
[email protected]9d2c8802014-09-03 02:04:46214 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))
[email protected]a112f032014-03-13 07:47:50223
[email protected]9d2c8802014-09-03 02:04:46224 # 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:
[email protected]9d2c8802014-09-03 02:04:46259 none_text = '' if self.__is_invalid_parent(branch) else 'None'
[email protected]ffde55c2015-03-12 00:44:17260 (url, color) = self.__status_info[branch]
261 line.append(url or none_text, color=color)
[email protected]9d2c8802014-09-03 02:04:46262
[email protected]09156ec2015-03-26 14:10:06263 # 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
[email protected]9d2c8802014-09-03 02:04:46267 self.output.append(line)
268
[email protected]745ffa62014-09-08 01:03:19269 for child in sorted(self.__parent_map.pop(branch, ())):
[email protected]9d2c8802014-09-03 02:04:46270 self.__append_branch(child, depth=depth + 1)
[email protected]8bc9b5c2014-03-12 01:36:18271
272
273def main(argv):
274 colorama.init()
[email protected]9d2c8802014-09-03 02:04:46275 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.')
[email protected]8bc9b5c2014-03-12 01:36:18281
[email protected]9d2c8802014-09-03 02:04:46282 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.')
[email protected]ffde55c2015-03-12 00:44:17288 parser.add_argument(
289 '-j', '--maxjobs', action='store', type=int,
290 help='The number of jobs to use when retrieving review status')
[email protected]09156ec2015-03-26 14:10:06291 parser.add_argument('--show-subject', action='store_true',
292 dest='show_subject', help='Show the commit subject.')
[email protected]8bc9b5c2014-03-12 01:36:18293
[email protected]013731e2015-02-26 18:28:43294 opts = parser.parse_args(argv)
[email protected]9d2c8802014-09-03 02:04:46295
296 mapper = BranchMapper()
297 mapper.verbosity = opts.v
298 mapper.output.nocolor = opts.nocolor
[email protected]ffde55c2015-03-12 00:44:17299 mapper.maxjobs = opts.maxjobs
[email protected]09156ec2015-03-26 14:10:06300 mapper.show_subject = opts.show_subject
[email protected]9d2c8802014-09-03 02:04:46301 mapper.start()
302 print mapper.output.as_formatted_string()
[email protected]013731e2015-02-26 18:28:43303 return 0
[email protected]8bc9b5c2014-03-12 01:36:18304
305if __name__ == '__main__':
[email protected]013731e2015-02-26 18:28:43306 try:
307 sys.exit(main(sys.argv[1:]))
308 except KeyboardInterrupt:
309 sys.stderr.write('interrupted\n')
310 sys.exit(1)