blob: 8627b3798c6cd7fb27af77d2a3e42d48d499e4d6 [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
anatoly techtonik222840f2017-04-15 13:25:576"""Print dependency tree of branches in local repo.
[email protected]8bc9b5c2014-03-12 01:36:187
[email protected]9d2c8802014-09-03 02:04:468Example:
[email protected]8bc9b5c2014-03-12 01:36:189origin/master
10 cool_feature
11 dependent_feature
12 other_dependent_feature
13 other_feature
14
15Branches are colorized as follows:
16 * Red - a remote branch (usually the root of all local branches)
17 * Cyan - a local branch which is the same as HEAD
18 * Note that multiple branches may be Cyan, if they are all on the same
19 commit, and you have that commit checked out.
20 * Green - a local branch
[email protected]4cd0a8b2014-09-23 03:30:5021 * Blue - a 'branch-heads' branch
[email protected]c050a5b2014-03-26 06:18:5022 * Magenta - a tag
23 * Magenta '{NO UPSTREAM}' - If you have local branches which do not track any
24 upstream, then you will see this.
[email protected]8bc9b5c2014-03-12 01:36:1825"""
[email protected]c050a5b2014-03-26 06:18:5026
[email protected]9d2c8802014-09-03 02:04:4627import argparse
[email protected]8bc9b5c2014-03-12 01:36:1828import collections
[email protected]0703ea22016-04-01 01:02:4229import os
[email protected]4cd0a8b2014-09-23 03:30:5030import subprocess2
[email protected]596cd5c2016-04-04 21:34:3931import sys
[email protected]8bc9b5c2014-03-12 01:36:1832
[email protected]745ffa62014-09-08 01:03:1933from git_common import current_branch, upstream, tags, get_branches_info
[email protected]4c82eb52014-09-08 02:12:2434from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION, hash_one
[email protected]09156ec2015-03-26 14:10:0635from git_common import run
[email protected]8bc9b5c2014-03-12 01:36:1836
[email protected]596cd5c2016-04-04 21:34:3937import setup_color
38
39from third_party.colorama import Fore, Style
40
[email protected]9d2c8802014-09-03 02:04:4641DEFAULT_SEPARATOR = ' ' * 4
[email protected]c050a5b2014-03-26 06:18:5042
43
[email protected]9d2c8802014-09-03 02:04:4644class OutputManager(object):
45 """Manages a number of OutputLines and formats them into aligned columns."""
[email protected]c050a5b2014-03-26 06:18:5046
[email protected]9d2c8802014-09-03 02:04:4647 def __init__(self):
48 self.lines = []
49 self.nocolor = False
50 self.max_column_lengths = []
51 self.num_columns = None
[email protected]c050a5b2014-03-26 06:18:5052
[email protected]9d2c8802014-09-03 02:04:4653 def append(self, line):
54 # All lines must have the same number of columns.
55 if not self.num_columns:
56 self.num_columns = len(line.columns)
57 self.max_column_lengths = [0] * self.num_columns
58 assert self.num_columns == len(line.columns)
59
60 if self.nocolor:
61 line.colors = [''] * self.num_columns
62
63 self.lines.append(line)
64
65 # Update maximum column lengths.
66 for i, col in enumerate(line.columns):
67 self.max_column_lengths[i] = max(self.max_column_lengths[i], len(col))
68
69 def as_formatted_string(self):
70 return '\n'.join(
71 l.as_padded_string(self.max_column_lengths) for l in self.lines)
72
73
74class OutputLine(object):
75 """A single line of data.
76
77 This consists of an equal number of columns, colors and separators."""
78
79 def __init__(self):
80 self.columns = []
81 self.separators = []
82 self.colors = []
83
84 def append(self, data, separator=DEFAULT_SEPARATOR, color=Fore.WHITE):
85 self.columns.append(data)
86 self.separators.append(separator)
87 self.colors.append(color)
88
89 def as_padded_string(self, max_column_lengths):
90 """"Returns the data as a string with each column padded to
91 |max_column_lengths|."""
92 output_string = ''
93 for i, (color, data, separator) in enumerate(
94 zip(self.colors, self.columns, self.separators)):
95 if max_column_lengths[i] == 0:
96 continue
97
98 padding = (max_column_lengths[i] - len(data)) * ' '
99 output_string += color + data + padding + separator
100
101 return output_string.rstrip()
102
103
104class BranchMapper(object):
105 """A class which constructs output representing the tree's branch structure.
106
107 Attributes:
[email protected]745ffa62014-09-08 01:03:19108 __branches_info: a map of branches to their BranchesInfo objects which
[email protected]9d2c8802014-09-03 02:04:46109 consist of the branch hash, upstream and ahead/behind status.
110 __gone_branches: a set of upstreams which are not fetchable by git"""
111
112 def __init__(self):
113 self.verbosity = 0
[email protected]ffde55c2015-03-12 00:44:17114 self.maxjobs = 0
[email protected]09156ec2015-03-26 14:10:06115 self.show_subject = False
[email protected]9d2c8802014-09-03 02:04:46116 self.output = OutputManager()
[email protected]9d2c8802014-09-03 02:04:46117 self.__gone_branches = set()
[email protected]745ffa62014-09-08 01:03:19118 self.__branches_info = None
119 self.__parent_map = collections.defaultdict(list)
120 self.__current_branch = None
121 self.__current_hash = None
122 self.__tag_set = None
[email protected]ffde55c2015-03-12 00:44:17123 self.__status_info = {}
[email protected]745ffa62014-09-08 01:03:19124
125 def start(self):
126 self.__branches_info = get_branches_info(
127 include_tracking_status=self.verbosity >= 1)
[email protected]ffde55c2015-03-12 00:44:17128 if (self.verbosity >= 2):
129 # Avoid heavy import unless necessary.
[email protected]cbd7dc32016-05-31 10:33:50130 from git_cl import get_cl_statuses, color_for_status, Changelist
[email protected]ffde55c2015-03-12 00:44:17131
[email protected]cbd7dc32016-05-31 10:33:50132 change_cls = [Changelist(branchref='refs/heads/'+b)
133 for b in self.__branches_info.keys() if b]
134 status_info = get_cl_statuses(change_cls,
[email protected]ffde55c2015-03-12 00:44:17135 fine_grained=self.verbosity > 2,
136 max_processes=self.maxjobs)
137
[email protected]cbd7dc32016-05-31 10:33:50138 # This is a blocking get which waits for the remote CL status to be
139 # retrieved.
140 for cl, status in status_info:
141 self.__status_info[cl.GetBranch()] = (cl.GetIssueURL(),
asanka97f39492016-07-19 01:16:40142 color_for_status(status),
143 status)
[email protected]ffde55c2015-03-12 00:44:17144
[email protected]745ffa62014-09-08 01:03:19145 roots = set()
[email protected]9d2c8802014-09-03 02:04:46146
147 # A map of parents to a list of their children.
[email protected]745ffa62014-09-08 01:03:19148 for branch, branch_info in self.__branches_info.iteritems():
[email protected]9d2c8802014-09-03 02:04:46149 if not branch_info:
150 continue
151
152 parent = branch_info.upstream
[email protected]4cd0a8b2014-09-23 03:30:50153 if not self.__branches_info[parent]:
[email protected]9d2c8802014-09-03 02:04:46154 branch_upstream = upstream(branch)
155 # If git can't find the upstream, mark the upstream as gone.
156 if branch_upstream:
157 parent = branch_upstream
158 else:
159 self.__gone_branches.add(parent)
[email protected]745ffa62014-09-08 01:03:19160 # A parent that isn't in the branches info is a root.
161 roots.add(parent)
[email protected]9d2c8802014-09-03 02:04:46162
[email protected]745ffa62014-09-08 01:03:19163 self.__parent_map[parent].append(branch)
[email protected]9d2c8802014-09-03 02:04:46164
165 self.__current_branch = current_branch()
[email protected]4c82eb52014-09-08 02:12:24166 self.__current_hash = hash_one('HEAD', short=True)
[email protected]9d2c8802014-09-03 02:04:46167 self.__tag_set = tags()
168
[email protected]4c82eb52014-09-08 02:12:24169 if roots:
170 for root in sorted(roots):
171 self.__append_branch(root)
172 else:
173 no_branches = OutputLine()
174 no_branches.append('No User Branches')
175 self.output.append(no_branches)
[email protected]9d2c8802014-09-03 02:04:46176
177 def __is_invalid_parent(self, parent):
178 return not parent or parent in self.__gone_branches
179
180 def __color_for_branch(self, branch, branch_hash):
[email protected]4f1fc352016-03-24 22:23:46181 if branch.startswith('origin/'):
[email protected]9d2c8802014-09-03 02:04:46182 color = Fore.RED
[email protected]4cd0a8b2014-09-23 03:30:50183 elif branch.startswith('branch-heads'):
184 color = Fore.BLUE
[email protected]9d2c8802014-09-03 02:04:46185 elif self.__is_invalid_parent(branch) or branch in self.__tag_set:
186 color = Fore.MAGENTA
[email protected]4c82eb52014-09-08 02:12:24187 elif self.__current_hash.startswith(branch_hash):
[email protected]9d2c8802014-09-03 02:04:46188 color = Fore.CYAN
189 else:
190 color = Fore.GREEN
191
[email protected]4cd0a8b2014-09-23 03:30:50192 if branch_hash and self.__current_hash.startswith(branch_hash):
[email protected]9d2c8802014-09-03 02:04:46193 color += Style.BRIGHT
194 else:
195 color += Style.NORMAL
196
197 return color
198
199 def __append_branch(self, branch, depth=0):
200 """Recurses through the tree structure and appends an OutputLine to the
201 OutputManager for each branch."""
[email protected]745ffa62014-09-08 01:03:19202 branch_info = self.__branches_info[branch]
[email protected]4c82eb52014-09-08 02:12:24203 if branch_info:
204 branch_hash = branch_info.hash
205 else:
[email protected]4cd0a8b2014-09-23 03:30:50206 try:
207 branch_hash = hash_one(branch, short=True)
208 except subprocess2.CalledProcessError:
209 branch_hash = None
[email protected]9d2c8802014-09-03 02:04:46210
211 line = OutputLine()
212
213 # The branch name with appropriate indentation.
214 suffix = ''
215 if branch == self.__current_branch or (
216 self.__current_branch == 'HEAD' and branch == self.__current_hash):
[email protected]a112f032014-03-13 07:47:50217 suffix = ' *'
[email protected]9d2c8802014-09-03 02:04:46218 branch_string = branch
219 if branch in self.__gone_branches:
220 branch_string = '{%s:GONE}' % branch
221 if not branch:
222 branch_string = '{NO_UPSTREAM}'
223 main_string = ' ' * depth + branch_string + suffix
224 line.append(
225 main_string,
226 color=self.__color_for_branch(branch, branch_hash))
[email protected]a112f032014-03-13 07:47:50227
[email protected]9d2c8802014-09-03 02:04:46228 # The branch hash.
229 if self.verbosity >= 2:
230 line.append(branch_hash or '', separator=' ', color=Fore.RED)
231
232 # The branch tracking status.
233 if self.verbosity >= 1:
234 ahead_string = ''
235 behind_string = ''
236 front_separator = ''
237 center_separator = ''
238 back_separator = ''
239 if branch_info and not self.__is_invalid_parent(branch_info.upstream):
240 ahead = branch_info.ahead
241 behind = branch_info.behind
242
243 if ahead:
244 ahead_string = 'ahead %d' % ahead
245 if behind:
246 behind_string = 'behind %d' % behind
247
248 if ahead or behind:
249 front_separator = '['
250 back_separator = ']'
251
252 if ahead and behind:
253 center_separator = '|'
254
255 line.append(front_separator, separator=' ')
256 line.append(ahead_string, separator=' ', color=Fore.MAGENTA)
257 line.append(center_separator, separator=' ')
258 line.append(behind_string, separator=' ', color=Fore.MAGENTA)
259 line.append(back_separator)
260
261 # The Rietveld issue associated with the branch.
262 if self.verbosity >= 2:
asanka97f39492016-07-19 01:16:40263 (url, color, status) = ('', '', '') if self.__is_invalid_parent(branch) \
264 else self.__status_info[branch]
265 if self.verbosity > 2:
266 line.append('{} ({})'.format(url, status) if url else '', color=color)
267 else:
268 line.append(url or '', color=color)
[email protected]9d2c8802014-09-03 02:04:46269
[email protected]09156ec2015-03-26 14:10:06270 # The subject of the most recent commit on the branch.
271 if self.show_subject:
Aaron Gable6761b9d2017-08-28 19:23:40272 if branch:
273 line.append(run('log', '-n1', '--format=%s', branch, '--'))
274 else:
275 line.append('')
[email protected]09156ec2015-03-26 14:10:06276
[email protected]9d2c8802014-09-03 02:04:46277 self.output.append(line)
278
[email protected]745ffa62014-09-08 01:03:19279 for child in sorted(self.__parent_map.pop(branch, ())):
[email protected]9d2c8802014-09-03 02:04:46280 self.__append_branch(child, depth=depth + 1)
[email protected]8bc9b5c2014-03-12 01:36:18281
282
anatoly techtonik222840f2017-04-15 13:25:57283def print_desc():
284 for line in __doc__.splitlines():
285 starpos = line.find('* ')
286 if starpos == -1 or '-' not in line:
287 print(line)
288 else:
289 _, color, rest = line.split(None, 2)
290 outline = line[:starpos+1]
291 outline += getattr(Fore, color.upper()) + " " + color + " " + Fore.RESET
292 outline += rest
293 print(outline)
294 print('')
295
[email protected]8bc9b5c2014-03-12 01:36:18296def main(argv):
[email protected]596cd5c2016-04-04 21:34:39297 setup_color.init()
[email protected]9d2c8802014-09-03 02:04:46298 if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
299 print >> sys.stderr, (
300 'This tool will not show all tracking information for git version '
301 'earlier than ' +
302 '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
303 '. Please consider upgrading.')
[email protected]8bc9b5c2014-03-12 01:36:18304
anatoly techtonik222840f2017-04-15 13:25:57305 if '-h' in argv:
306 print_desc()
307
308 parser = argparse.ArgumentParser()
[email protected]9d2c8802014-09-03 02:04:46309 parser.add_argument('-v', action='count',
Aaron Gablefe2f3802017-08-28 19:17:21310 help=('Pass once to show tracking info; '
311 'twice for hash and review url'))
[email protected]9d2c8802014-09-03 02:04:46312 parser.add_argument('--no-color', action='store_true', dest='nocolor',
313 help='Turn off colors.')
[email protected]ffde55c2015-03-12 00:44:17314 parser.add_argument(
315 '-j', '--maxjobs', action='store', type=int,
316 help='The number of jobs to use when retrieving review status')
[email protected]09156ec2015-03-26 14:10:06317 parser.add_argument('--show-subject', action='store_true',
318 dest='show_subject', help='Show the commit subject.')
[email protected]8bc9b5c2014-03-12 01:36:18319
[email protected]013731e2015-02-26 18:28:43320 opts = parser.parse_args(argv)
[email protected]9d2c8802014-09-03 02:04:46321
322 mapper = BranchMapper()
323 mapper.verbosity = opts.v
324 mapper.output.nocolor = opts.nocolor
[email protected]ffde55c2015-03-12 00:44:17325 mapper.maxjobs = opts.maxjobs
[email protected]09156ec2015-03-26 14:10:06326 mapper.show_subject = opts.show_subject
[email protected]9d2c8802014-09-03 02:04:46327 mapper.start()
328 print mapper.output.as_formatted_string()
[email protected]013731e2015-02-26 18:28:43329 return 0
[email protected]8bc9b5c2014-03-12 01:36:18330
331if __name__ == '__main__':
[email protected]013731e2015-02-26 18:28:43332 try:
333 sys.exit(main(sys.argv[1:]))
334 except KeyboardInterrupt:
335 sys.stderr.write('interrupted\n')
336 sys.exit(1)