Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(83)

Side by Side Diff: git_map_branches.py

Issue 509843002: Give git map-branches extra information behind -v and -vv flags. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: make main loop nicer, privatize tags Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved. 2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """ 6 """Provides a short mapping of all the branches in your local repo, organized
7 Provides a short mapping of all the branches in your local repo, organized by 7 by their upstream ('tracking branch') layout.
8 their upstream ('tracking branch') layout. Example:
9 8
9 Example:
10 origin/master 10 origin/master
11 cool_feature 11 cool_feature
12 dependent_feature 12 dependent_feature
13 other_dependent_feature 13 other_dependent_feature
14 other_feature 14 other_feature
15 15
16 Branches are colorized as follows: 16 Branches are colorized as follows:
17 * Red - a remote branch (usually the root of all local branches) 17 * Red - a remote branch (usually the root of all local branches)
18 * Cyan - a local branch which is the same as HEAD 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 19 * Note that multiple branches may be Cyan, if they are all on the same
20 commit, and you have that commit checked out. 20 commit, and you have that commit checked out.
21 * Green - a local branch 21 * Green - a local branch
22 * Magenta - a tag 22 * Magenta - a tag
23 * Magenta '{NO UPSTREAM}' - If you have local branches which do not track any 23 * Magenta '{NO UPSTREAM}' - If you have local branches which do not track any
24 upstream, then you will see this. 24 upstream, then you will see this.
25 """ 25 """
26 26
27 import argparse
27 import collections 28 import collections
28 import sys 29 import sys
29 30
30 from third_party import colorama 31 from third_party import colorama
31 from third_party.colorama import Fore, Style 32 from third_party.colorama import Fore, Style
32 33
33 from git_common import current_branch, branches, upstream, hash_one, hash_multi 34 from git_common import current_branch, upstream, tags, get_all_tracking_info
34 from git_common import tags 35 from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION
35 36
36 NO_UPSTREAM = '{NO UPSTREAM}' 37 import git_cl
37 38
38 def color_for_branch(branch, branch_hash, cur_hash, tag_set): 39 DEFAULT_SEPARATOR = ' ' * 4
39 if branch.startswith('origin'): 40
40 color = Fore.RED 41
41 elif branch == NO_UPSTREAM or branch in tag_set: 42 class OutputManager(object):
42 color = Fore.MAGENTA 43 """Manages a number of OutputLines and formats them into aligned columns."""
43 elif branch_hash == cur_hash: 44
44 color = Fore.CYAN 45 def __init__(self):
45 else: 46 self.lines = []
46 color = Fore.GREEN 47 self.nocolor = False
47 48 self.max_column_lengths = []
48 if branch_hash == cur_hash: 49 self.num_columns = None
49 color += Style.BRIGHT 50
50 else: 51 def append(self, line):
51 color += Style.NORMAL 52 # All lines must have the same number of columns.
52 53 if not self.num_columns:
53 return color 54 self.num_columns = len(line.columns)
54 55 self.max_column_lengths = [0] * self.num_columns
55 56 assert self.num_columns == len(line.columns)
56 def print_branch(cur, cur_hash, branch, branch_hashes, par_map, branch_map, 57
57 tag_set, depth=0): 58 if self.nocolor:
58 branch_hash = branch_hashes[branch] 59 line.colors = [''] * self.num_columns
59 60
60 color = color_for_branch(branch, branch_hash, cur_hash, tag_set) 61 self.lines.append(line)
61 62
62 suffix = '' 63 # Update maximum column lengths.
63 if cur == 'HEAD': 64 for i, col in enumerate(line.columns):
64 if branch_hash == cur_hash: 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
72 class 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
102 class 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
116 # A map of parents to a list of their children.
117 self.parent_map = collections.defaultdict(list)
118 for branch, branch_info in self.__tracking_info.iteritems():
119 if not branch_info.hash:
120 continue
121
122 parent = branch_info.upstream
123 if parent and not self.__tracking_info[parent].hash:
124 branch_upstream = upstream(branch)
125 # If git can't find the upstream, mark the upstream as gone.
126 if branch_upstream:
127 parent = branch_upstream
128 else:
129 self.__gone_branches.add(parent)
130 self.parent_map[parent].append(branch)
131
132 self.__current_branch = current_branch()
133 self.__current_hash = self.__tracking_info[self.__current_branch].hash
134 self.__tag_set = tags()
135
136 def start(self):
137 # Find each branch without an upstream and append its subtree.
138 roots = (
139 branch for branch, branch_info in self.__tracking_info.iteritems()
140 if not branch_info.upstream)
141 for root in sorted(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_hash = self.__tracking_info[branch].hash
168
169 line = OutputLine()
170
171 # The branch name with appropriate indentation.
172 suffix = ''
173 if branch == self.__current_branch or (
174 self.__current_branch == 'HEAD' and branch == self.__current_hash):
65 suffix = ' *' 175 suffix = ' *'
66 elif branch == cur: 176 branch_string = branch
67 suffix = ' *' 177 if branch in self.__gone_branches:
68 178 branch_string = '{%s:GONE}' % branch
69 print color + " "*depth + branch + suffix 179 if not branch:
70 for child in par_map.pop(branch, ()): 180 branch_string = '{NO_UPSTREAM}'
71 print_branch(cur, cur_hash, child, branch_hashes, par_map, branch_map, 181 main_string = ' ' * depth + branch_string + suffix
72 tag_set, depth=depth+1) 182 line.append(
183 main_string,
184 color=self.__color_for_branch(branch, branch_hash))
185
186 # The branch hash.
187 if self.verbosity >= 1:
188 line.append(branch_hash or '', separator=' ', color=Fore.RED)
189
190 # The branch tracking status.
191 ahead_string = ''
192 behind_string = ''
193 front_separator = ''
194 center_separator = ''
195 back_separator = ''
196 branch_info = self.__tracking_info.get(branch, None)
197 if branch_info and not self.__is_invalid_parent(branch_info.upstream):
198 ahead = branch_info.ahead
199 behind = branch_info.behind
200
201 if ahead:
202 ahead_string = 'ahead ' + str(ahead)
203 if behind:
204 behind_string = 'behind ' + str(behind)
iannucci 2014/09/02 22:31:27 I'd use formatting for this 'behind %d' % behin
Matt Giuca 2014/09/02 23:43:33 +1. In general, prefer formatting over +.
calamity 2014/09/03 01:43:21 Done.
205
206 if ahead or behind:
207 front_separator = '['
208 back_separator = ']'
209
210 if ahead and behind:
211 center_separator = '|'
212
213 line.append(front_separator, separator=' ')
214 line.append(ahead_string, separator=' ', color=Fore.MAGENTA)
215 line.append(center_separator, separator=' ')
216 line.append(behind_string, separator=' ', color=Fore.MAGENTA)
217 line.append(back_separator)
218
219 # The Rietveld issue associated with the branch.
220 if self.verbosity >= 1:
221 none_text = '' if self.__is_invalid_parent(branch) else 'None'
222 url = git_cl.Changelist(branchref=branch).GetIssueURL()
223 line.append(url or none_text, color=Fore.BLUE if url else Fore.WHITE)
224
225 self.output.append(line)
226
227 for child in sorted(self.parent_map.pop(branch, ())):
228 self.__append_branch(child, depth=depth + 1)
73 229
74 230
75 def main(argv): 231 def main(argv):
76 colorama.init() 232 colorama.init()
77 assert len(argv) == 1, "No arguments expected" 233 if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
78 branch_map = {} 234 print >> sys.stderr, (
79 par_map = collections.defaultdict(list) 235 'This tool will not show all tracking information for git version '
80 for branch in branches(): 236 'earlier than ' +
81 par = upstream(branch) or NO_UPSTREAM 237 '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
82 branch_map[branch] = par 238 '. Please consider upgrading.')
83 par_map[par].append(branch) 239
84 240 parser = argparse.ArgumentParser(
85 current = current_branch() 241 description='Print a a tree of all branches parented by their upstreams')
86 hashes = hash_multi(current, *branch_map.keys()) 242 parser.add_argument('-v', action='count',
87 current_hash = hashes[0] 243 help='Display branch hash and Rietveld URL')
88 par_hashes = {k: hashes[i+1] for i, k in enumerate(branch_map.iterkeys())} 244 parser.add_argument('--no-color', action='store_true', dest='nocolor',
89 par_hashes[NO_UPSTREAM] = 0 245 help='Turn off colors.')
90 tag_set = tags() 246
91 while par_map: 247 opts = parser.parse_args(argv[1:])
92 for parent in par_map: 248
93 if parent not in branch_map: 249 mapper = BranchMapper()
94 if parent not in par_hashes: 250 mapper.verbosity = opts.v
95 par_hashes[parent] = hash_one(parent) 251 mapper.output.nocolor = opts.nocolor
96 print_branch(current, current_hash, parent, par_hashes, par_map, 252 mapper.start()
97 branch_map, tag_set) 253 print mapper.output.as_formatted_string()
98 break
99
100 254
101 if __name__ == '__main__': 255 if __name__ == '__main__':
102 sys.exit(main(sys.argv)) 256 sys.exit(main(sys.argv))
103
OLDNEW
« git_common.py ('K') | « git_common.py ('k') | tests/git_common_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698