blob: 44f13c3bd5abc010269e48d05d6d82a7374ddf65 [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]8bc9b5c2014-03-12 01:36:1838
[email protected]9d2c8802014-09-03 02:04:4639DEFAULT_SEPARATOR = ' ' * 4
[email protected]c050a5b2014-03-26 06:18:5040
41
[email protected]9d2c8802014-09-03 02:04:4642class OutputManager(object):
43 """Manages a number of OutputLines and formats them into aligned columns."""
[email protected]c050a5b2014-03-26 06:18:5044
[email protected]9d2c8802014-09-03 02:04:4645 def __init__(self):
46 self.lines = []
47 self.nocolor = False
48 self.max_column_lengths = []
49 self.num_columns = None
[email protected]c050a5b2014-03-26 06:18:5050
[email protected]9d2c8802014-09-03 02:04:4651 def append(self, line):
52 # All lines must have the same number of columns.
53 if not self.num_columns:
54 self.num_columns = len(line.columns)
55 self.max_column_lengths = [0] * self.num_columns
56 assert self.num_columns == len(line.columns)
57
58 if self.nocolor:
59 line.colors = [''] * self.num_columns
60
61 self.lines.append(line)
62
63 # Update maximum column lengths.
64 for i, col in enumerate(line.columns):
65 self.max_column_lengths[i] = max(self.max_column_lengths[i], len(col))
66
67 def as_formatted_string(self):
68 return '\n'.join(
69 l.as_padded_string(self.max_column_lengths) for l in self.lines)
70
71
72class OutputLine(object):
73 """A single line of data.
74
75 This consists of an equal number of columns, colors and separators."""
76
77 def __init__(self):
78 self.columns = []
79 self.separators = []
80 self.colors = []
81
82 def append(self, data, separator=DEFAULT_SEPARATOR, color=Fore.WHITE):
83 self.columns.append(data)
84 self.separators.append(separator)
85 self.colors.append(color)
86
87 def as_padded_string(self, max_column_lengths):
88 """"Returns the data as a string with each column padded to
89 |max_column_lengths|."""
90 output_string = ''
91 for i, (color, data, separator) in enumerate(
92 zip(self.colors, self.columns, self.separators)):
93 if max_column_lengths[i] == 0:
94 continue
95
96 padding = (max_column_lengths[i] - len(data)) * ' '
97 output_string += color + data + padding + separator
98
99 return output_string.rstrip()
100
101
102class BranchMapper(object):
103 """A class which constructs output representing the tree's branch structure.
104
105 Attributes:
[email protected]745ffa62014-09-08 01:03:19106 __branches_info: a map of branches to their BranchesInfo objects which
[email protected]9d2c8802014-09-03 02:04:46107 consist of the branch hash, upstream and ahead/behind status.
108 __gone_branches: a set of upstreams which are not fetchable by git"""
109
110 def __init__(self):
111 self.verbosity = 0
112 self.output = OutputManager()
[email protected]9d2c8802014-09-03 02:04:46113 self.__gone_branches = set()
[email protected]745ffa62014-09-08 01:03:19114 self.__branches_info = None
115 self.__parent_map = collections.defaultdict(list)
116 self.__current_branch = None
117 self.__current_hash = None
118 self.__tag_set = None
119
120 def start(self):
121 self.__branches_info = get_branches_info(
122 include_tracking_status=self.verbosity >= 1)
123 roots = set()
[email protected]9d2c8802014-09-03 02:04:46124
125 # A map of parents to a list of their children.
[email protected]745ffa62014-09-08 01:03:19126 for branch, branch_info in self.__branches_info.iteritems():
[email protected]9d2c8802014-09-03 02:04:46127 if not branch_info:
128 continue
129
130 parent = branch_info.upstream
[email protected]4cd0a8b2014-09-23 03:30:50131 if not self.__branches_info[parent]:
[email protected]9d2c8802014-09-03 02:04:46132 branch_upstream = upstream(branch)
133 # If git can't find the upstream, mark the upstream as gone.
134 if branch_upstream:
135 parent = branch_upstream
136 else:
137 self.__gone_branches.add(parent)
[email protected]745ffa62014-09-08 01:03:19138 # A parent that isn't in the branches info is a root.
139 roots.add(parent)
[email protected]9d2c8802014-09-03 02:04:46140
[email protected]745ffa62014-09-08 01:03:19141 self.__parent_map[parent].append(branch)
[email protected]9d2c8802014-09-03 02:04:46142
143 self.__current_branch = current_branch()
[email protected]4c82eb52014-09-08 02:12:24144 self.__current_hash = hash_one('HEAD', short=True)
[email protected]9d2c8802014-09-03 02:04:46145 self.__tag_set = tags()
146
[email protected]4c82eb52014-09-08 02:12:24147 if roots:
148 for root in sorted(roots):
149 self.__append_branch(root)
150 else:
151 no_branches = OutputLine()
152 no_branches.append('No User Branches')
153 self.output.append(no_branches)
[email protected]9d2c8802014-09-03 02:04:46154
155 def __is_invalid_parent(self, parent):
156 return not parent or parent in self.__gone_branches
157
158 def __color_for_branch(self, branch, branch_hash):
159 if branch.startswith('origin'):
160 color = Fore.RED
[email protected]4cd0a8b2014-09-23 03:30:50161 elif branch.startswith('branch-heads'):
162 color = Fore.BLUE
[email protected]9d2c8802014-09-03 02:04:46163 elif self.__is_invalid_parent(branch) or branch in self.__tag_set:
164 color = Fore.MAGENTA
[email protected]4c82eb52014-09-08 02:12:24165 elif self.__current_hash.startswith(branch_hash):
[email protected]9d2c8802014-09-03 02:04:46166 color = Fore.CYAN
167 else:
168 color = Fore.GREEN
169
[email protected]4cd0a8b2014-09-23 03:30:50170 if branch_hash and self.__current_hash.startswith(branch_hash):
[email protected]9d2c8802014-09-03 02:04:46171 color += Style.BRIGHT
172 else:
173 color += Style.NORMAL
174
175 return color
176
177 def __append_branch(self, branch, depth=0):
178 """Recurses through the tree structure and appends an OutputLine to the
179 OutputManager for each branch."""
[email protected]745ffa62014-09-08 01:03:19180 branch_info = self.__branches_info[branch]
[email protected]4c82eb52014-09-08 02:12:24181 if branch_info:
182 branch_hash = branch_info.hash
183 else:
[email protected]4cd0a8b2014-09-23 03:30:50184 try:
185 branch_hash = hash_one(branch, short=True)
186 except subprocess2.CalledProcessError:
187 branch_hash = None
[email protected]9d2c8802014-09-03 02:04:46188
189 line = OutputLine()
190
191 # The branch name with appropriate indentation.
192 suffix = ''
193 if branch == self.__current_branch or (
194 self.__current_branch == 'HEAD' and branch == self.__current_hash):
[email protected]a112f032014-03-13 07:47:50195 suffix = ' *'
[email protected]9d2c8802014-09-03 02:04:46196 branch_string = branch
197 if branch in self.__gone_branches:
198 branch_string = '{%s:GONE}' % branch
199 if not branch:
200 branch_string = '{NO_UPSTREAM}'
201 main_string = ' ' * depth + branch_string + suffix
202 line.append(
203 main_string,
204 color=self.__color_for_branch(branch, branch_hash))
[email protected]a112f032014-03-13 07:47:50205
[email protected]9d2c8802014-09-03 02:04:46206 # The branch hash.
207 if self.verbosity >= 2:
208 line.append(branch_hash or '', separator=' ', color=Fore.RED)
209
210 # The branch tracking status.
211 if self.verbosity >= 1:
212 ahead_string = ''
213 behind_string = ''
214 front_separator = ''
215 center_separator = ''
216 back_separator = ''
217 if branch_info and not self.__is_invalid_parent(branch_info.upstream):
218 ahead = branch_info.ahead
219 behind = branch_info.behind
220
221 if ahead:
222 ahead_string = 'ahead %d' % ahead
223 if behind:
224 behind_string = 'behind %d' % behind
225
226 if ahead or behind:
227 front_separator = '['
228 back_separator = ']'
229
230 if ahead and behind:
231 center_separator = '|'
232
233 line.append(front_separator, separator=' ')
234 line.append(ahead_string, separator=' ', color=Fore.MAGENTA)
235 line.append(center_separator, separator=' ')
236 line.append(behind_string, separator=' ', color=Fore.MAGENTA)
237 line.append(back_separator)
238
239 # The Rietveld issue associated with the branch.
240 if self.verbosity >= 2:
[email protected]4c82eb52014-09-08 02:12:24241 import git_cl # avoid heavy import cost unless we need it
[email protected]9d2c8802014-09-03 02:04:46242 none_text = '' if self.__is_invalid_parent(branch) else 'None'
[email protected]4cd0a8b2014-09-23 03:30:50243 url = git_cl.Changelist(
244 branchref=branch).GetIssueURL() if branch_hash else None
[email protected]9d2c8802014-09-03 02:04:46245 line.append(url or none_text, color=Fore.BLUE if url else Fore.WHITE)
246
247 self.output.append(line)
248
[email protected]745ffa62014-09-08 01:03:19249 for child in sorted(self.__parent_map.pop(branch, ())):
[email protected]9d2c8802014-09-03 02:04:46250 self.__append_branch(child, depth=depth + 1)
[email protected]8bc9b5c2014-03-12 01:36:18251
252
253def main(argv):
254 colorama.init()
[email protected]9d2c8802014-09-03 02:04:46255 if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
256 print >> sys.stderr, (
257 'This tool will not show all tracking information for git version '
258 'earlier than ' +
259 '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
260 '. Please consider upgrading.')
[email protected]8bc9b5c2014-03-12 01:36:18261
[email protected]9d2c8802014-09-03 02:04:46262 parser = argparse.ArgumentParser(
263 description='Print a a tree of all branches parented by their upstreams')
264 parser.add_argument('-v', action='count',
265 help='Display branch hash and Rietveld URL')
266 parser.add_argument('--no-color', action='store_true', dest='nocolor',
267 help='Turn off colors.')
[email protected]8bc9b5c2014-03-12 01:36:18268
[email protected]9d2c8802014-09-03 02:04:46269 opts = parser.parse_args(argv[1:])
270
271 mapper = BranchMapper()
272 mapper.verbosity = opts.v
273 mapper.output.nocolor = opts.nocolor
274 mapper.start()
275 print mapper.output.as_formatted_string()
[email protected]8bc9b5c2014-03-12 01:36:18276
277if __name__ == '__main__':
278 sys.exit(main(sys.argv))