blob: 350fed37a15551bbb5f54791dfb6f6df32921f52 [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.org745ffa62014-09-08 01:03:19 +000034from git_common import current_branch, upstream, tags, get_branches_info
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +000035from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION, hash_one
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +000036
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000037DEFAULT_SEPARATOR = ' ' * 4
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000038
39
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000040class OutputManager(object):
41 """Manages a number of OutputLines and formats them into aligned columns."""
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000042
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000043 def __init__(self):
44 self.lines = []
45 self.nocolor = False
46 self.max_column_lengths = []
47 self.num_columns = None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000048
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000049 def append(self, line):
50 # All lines must have the same number of columns.
51 if not self.num_columns:
52 self.num_columns = len(line.columns)
53 self.max_column_lengths = [0] * self.num_columns
54 assert self.num_columns == len(line.columns)
55
56 if self.nocolor:
57 line.colors = [''] * self.num_columns
58
59 self.lines.append(line)
60
61 # Update maximum column lengths.
62 for i, col in enumerate(line.columns):
63 self.max_column_lengths[i] = max(self.max_column_lengths[i], len(col))
64
65 def as_formatted_string(self):
66 return '\n'.join(
67 l.as_padded_string(self.max_column_lengths) for l in self.lines)
68
69
70class OutputLine(object):
71 """A single line of data.
72
73 This consists of an equal number of columns, colors and separators."""
74
75 def __init__(self):
76 self.columns = []
77 self.separators = []
78 self.colors = []
79
80 def append(self, data, separator=DEFAULT_SEPARATOR, color=Fore.WHITE):
81 self.columns.append(data)
82 self.separators.append(separator)
83 self.colors.append(color)
84
85 def as_padded_string(self, max_column_lengths):
86 """"Returns the data as a string with each column padded to
87 |max_column_lengths|."""
88 output_string = ''
89 for i, (color, data, separator) in enumerate(
90 zip(self.colors, self.columns, self.separators)):
91 if max_column_lengths[i] == 0:
92 continue
93
94 padding = (max_column_lengths[i] - len(data)) * ' '
95 output_string += color + data + padding + separator
96
97 return output_string.rstrip()
98
99
100class BranchMapper(object):
101 """A class which constructs output representing the tree's branch structure.
102
103 Attributes:
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000104 __branches_info: a map of branches to their BranchesInfo objects which
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000105 consist of the branch hash, upstream and ahead/behind status.
106 __gone_branches: a set of upstreams which are not fetchable by git"""
107
108 def __init__(self):
109 self.verbosity = 0
110 self.output = OutputManager()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000111 self.__gone_branches = set()
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000112 self.__branches_info = None
113 self.__parent_map = collections.defaultdict(list)
114 self.__current_branch = None
115 self.__current_hash = None
116 self.__tag_set = None
117
118 def start(self):
119 self.__branches_info = get_branches_info(
120 include_tracking_status=self.verbosity >= 1)
121 roots = set()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000122
123 # A map of parents to a list of their children.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000124 for branch, branch_info in self.__branches_info.iteritems():
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000125 if not branch_info:
126 continue
127
128 parent = branch_info.upstream
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000129 if parent and not self.__branches_info[parent]:
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000130 branch_upstream = upstream(branch)
131 # If git can't find the upstream, mark the upstream as gone.
132 if branch_upstream:
133 parent = branch_upstream
134 else:
135 self.__gone_branches.add(parent)
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000136 # A parent that isn't in the branches info is a root.
137 roots.add(parent)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000138
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000139 self.__parent_map[parent].append(branch)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000140
141 self.__current_branch = current_branch()
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000142 self.__current_hash = hash_one('HEAD', short=True)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000143 self.__tag_set = tags()
144
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000145 if roots:
146 for root in sorted(roots):
147 self.__append_branch(root)
148 else:
149 no_branches = OutputLine()
150 no_branches.append('No User Branches')
151 self.output.append(no_branches)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000152
153 def __is_invalid_parent(self, parent):
154 return not parent or parent in self.__gone_branches
155
156 def __color_for_branch(self, branch, branch_hash):
157 if branch.startswith('origin'):
158 color = Fore.RED
159 elif self.__is_invalid_parent(branch) or branch in self.__tag_set:
160 color = Fore.MAGENTA
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000161 elif self.__current_hash.startswith(branch_hash):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000162 color = Fore.CYAN
163 else:
164 color = Fore.GREEN
165
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000166 if self.__current_hash.startswith(branch_hash):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000167 color += Style.BRIGHT
168 else:
169 color += Style.NORMAL
170
171 return color
172
173 def __append_branch(self, branch, depth=0):
174 """Recurses through the tree structure and appends an OutputLine to the
175 OutputManager for each branch."""
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000176 branch_info = self.__branches_info[branch]
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000177 if branch_info:
178 branch_hash = branch_info.hash
179 else:
180 branch_hash = hash_one(branch, short=True)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000181
182 line = OutputLine()
183
184 # The branch name with appropriate indentation.
185 suffix = ''
186 if branch == self.__current_branch or (
187 self.__current_branch == 'HEAD' and branch == self.__current_hash):
iannucci@chromium.orga112f032014-03-13 07:47:50 +0000188 suffix = ' *'
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000189 branch_string = branch
190 if branch in self.__gone_branches:
191 branch_string = '{%s:GONE}' % branch
192 if not branch:
193 branch_string = '{NO_UPSTREAM}'
194 main_string = ' ' * depth + branch_string + suffix
195 line.append(
196 main_string,
197 color=self.__color_for_branch(branch, branch_hash))
iannucci@chromium.orga112f032014-03-13 07:47:50 +0000198
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000199 # The branch hash.
200 if self.verbosity >= 2:
201 line.append(branch_hash or '', separator=' ', color=Fore.RED)
202
203 # The branch tracking status.
204 if self.verbosity >= 1:
205 ahead_string = ''
206 behind_string = ''
207 front_separator = ''
208 center_separator = ''
209 back_separator = ''
210 if branch_info and not self.__is_invalid_parent(branch_info.upstream):
211 ahead = branch_info.ahead
212 behind = branch_info.behind
213
214 if ahead:
215 ahead_string = 'ahead %d' % ahead
216 if behind:
217 behind_string = 'behind %d' % behind
218
219 if ahead or behind:
220 front_separator = '['
221 back_separator = ']'
222
223 if ahead and behind:
224 center_separator = '|'
225
226 line.append(front_separator, separator=' ')
227 line.append(ahead_string, separator=' ', color=Fore.MAGENTA)
228 line.append(center_separator, separator=' ')
229 line.append(behind_string, separator=' ', color=Fore.MAGENTA)
230 line.append(back_separator)
231
232 # The Rietveld issue associated with the branch.
233 if self.verbosity >= 2:
iannucci@chromium.org4c82eb52014-09-08 02:12:24 +0000234 import git_cl # avoid heavy import cost unless we need it
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000235 none_text = '' if self.__is_invalid_parent(branch) else 'None'
236 url = git_cl.Changelist(branchref=branch).GetIssueURL()
237 line.append(url or none_text, color=Fore.BLUE if url else Fore.WHITE)
238
239 self.output.append(line)
240
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000241 for child in sorted(self.__parent_map.pop(branch, ())):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000242 self.__append_branch(child, depth=depth + 1)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000243
244
245def main(argv):
246 colorama.init()
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000247 if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
248 print >> sys.stderr, (
249 'This tool will not show all tracking information for git version '
250 'earlier than ' +
251 '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
252 '. Please consider upgrading.')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000253
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000254 parser = argparse.ArgumentParser(
255 description='Print a a tree of all branches parented by their upstreams')
256 parser.add_argument('-v', action='count',
257 help='Display branch hash and Rietveld URL')
258 parser.add_argument('--no-color', action='store_true', dest='nocolor',
259 help='Turn off colors.')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000260
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000261 opts = parser.parse_args(argv[1:])
262
263 mapper = BranchMapper()
264 mapper.verbosity = opts.v
265 mapper.output.nocolor = opts.nocolor
266 mapper.start()
267 print mapper.output.as_formatted_string()
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000268
269if __name__ == '__main__':
270 sys.exit(main(sys.argv))