blob: e18202b56349c9a406b10fe87c02a441911617b0 [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
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000022 * Magenta - a tag
23 * Magenta '{NO UPSTREAM}' - If you have local branches which do not track any
24 upstream, then you will see this.
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000025"""
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000026
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000027import argparse
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000028import collections
29import sys
30
31from third_party import colorama
32from third_party.colorama import Fore, Style
33
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000034from git_common import current_branch, upstream, tags, get_all_tracking_info
35from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000036
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000037import git_cl
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000038
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000039DEFAULT_SEPARATOR = ' ' * 4
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000040
41
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000042class OutputManager(object):
43 """Manages a number of OutputLines and formats them into aligned columns."""
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000044
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000045 def __init__(self):
46 self.lines = []
47 self.nocolor = False
48 self.max_column_lengths = []
49 self.num_columns = None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000050
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000051 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:
106 __tracking_info: a map of branches to their TrackingInfo objects which
107 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()
113 self.__tracking_info = get_all_tracking_info()
114 self.__gone_branches = set()
115 self.__roots = set()
116
117 # A map of parents to a list of their children.
118 self.parent_map = collections.defaultdict(list)
119 for branch, branch_info in self.__tracking_info.iteritems():
120 if not branch_info:
121 continue
122
123 parent = branch_info.upstream
124 if parent and not self.__tracking_info[parent]:
125 branch_upstream = upstream(branch)
126 # If git can't find the upstream, mark the upstream as gone.
127 if branch_upstream:
128 parent = branch_upstream
129 else:
130 self.__gone_branches.add(parent)
131 # A parent that isn't in the tracking info is a root.
132 self.__roots.add(parent)
133
134 self.parent_map[parent].append(branch)
135
136 self.__current_branch = current_branch()
137 self.__current_hash = self.__tracking_info[self.__current_branch].hash
138 self.__tag_set = tags()
139
140 def start(self):
141 for root in sorted(self.__roots):
142 self.__append_branch(root)
143
144 def __is_invalid_parent(self, parent):
145 return not parent or parent in self.__gone_branches
146
147 def __color_for_branch(self, branch, branch_hash):
148 if branch.startswith('origin'):
149 color = Fore.RED
150 elif self.__is_invalid_parent(branch) or branch in self.__tag_set:
151 color = Fore.MAGENTA
152 elif branch_hash == self.__current_hash:
153 color = Fore.CYAN
154 else:
155 color = Fore.GREEN
156
157 if branch_hash == self.__current_hash:
158 color += Style.BRIGHT
159 else:
160 color += Style.NORMAL
161
162 return color
163
164 def __append_branch(self, branch, depth=0):
165 """Recurses through the tree structure and appends an OutputLine to the
166 OutputManager for each branch."""
167 branch_info = self.__tracking_info[branch]
168 branch_hash = branch_info.hash if branch_info else None
169
170 line = OutputLine()
171
172 # The branch name with appropriate indentation.
173 suffix = ''
174 if branch == self.__current_branch or (
175 self.__current_branch == 'HEAD' and branch == self.__current_hash):
iannucci@chromium.orga112f032014-03-13 07:47:50 +0000176 suffix = ' *'
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000177 branch_string = branch
178 if branch in self.__gone_branches:
179 branch_string = '{%s:GONE}' % branch
180 if not branch:
181 branch_string = '{NO_UPSTREAM}'
182 main_string = ' ' * depth + branch_string + suffix
183 line.append(
184 main_string,
185 color=self.__color_for_branch(branch, branch_hash))
iannucci@chromium.orga112f032014-03-13 07:47:50 +0000186
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000187 # The branch hash.
188 if self.verbosity >= 2:
189 line.append(branch_hash or '', separator=' ', color=Fore.RED)
190
191 # The branch tracking status.
192 if self.verbosity >= 1:
193 ahead_string = ''
194 behind_string = ''
195 front_separator = ''
196 center_separator = ''
197 back_separator = ''
198 if branch_info and not self.__is_invalid_parent(branch_info.upstream):
199 ahead = branch_info.ahead
200 behind = branch_info.behind
201
202 if ahead:
203 ahead_string = 'ahead %d' % ahead
204 if behind:
205 behind_string = 'behind %d' % behind
206
207 if ahead or behind:
208 front_separator = '['
209 back_separator = ']'
210
211 if ahead and behind:
212 center_separator = '|'
213
214 line.append(front_separator, separator=' ')
215 line.append(ahead_string, separator=' ', color=Fore.MAGENTA)
216 line.append(center_separator, separator=' ')
217 line.append(behind_string, separator=' ', color=Fore.MAGENTA)
218 line.append(back_separator)
219
220 # The Rietveld issue associated with the branch.
221 if self.verbosity >= 2:
222 none_text = '' if self.__is_invalid_parent(branch) else 'None'
223 url = git_cl.Changelist(branchref=branch).GetIssueURL()
224 line.append(url or none_text, color=Fore.BLUE if url else Fore.WHITE)
225
226 self.output.append(line)
227
228 for child in sorted(self.parent_map.pop(branch, ())):
229 self.__append_branch(child, depth=depth + 1)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000230
231
232def main(argv):
233 colorama.init()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000234 if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
235 print >> sys.stderr, (
236 'This tool will not show all tracking information for git version '
237 'earlier than ' +
238 '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
239 '. Please consider upgrading.')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000240
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000241 parser = argparse.ArgumentParser(
242 description='Print a a tree of all branches parented by their upstreams')
243 parser.add_argument('-v', action='count',
244 help='Display branch hash and Rietveld URL')
245 parser.add_argument('--no-color', action='store_true', dest='nocolor',
246 help='Turn off colors.')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000247
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000248 opts = parser.parse_args(argv[1:])
249
250 mapper = BranchMapper()
251 mapper.verbosity = opts.v
252 mapper.output.nocolor = opts.nocolor
253 mapper.start()
254 print mapper.output.as_formatted_string()
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000255
256if __name__ == '__main__':
257 sys.exit(main(sys.argv))