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

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: handle deleted upstreams more informatively 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, hash_one
34 from git_common import tags 35 from git_common import tags, get_all_tracking_info, normalized_version
36 from git_common import MIN_UPSTREAM_TRACK_GIT_VERSION
37
38 import git_cl
35 39
36 NO_UPSTREAM = '{NO UPSTREAM}' 40 NO_UPSTREAM = '{NO UPSTREAM}'
37 41 DEFAULT_SEPARATOR = ' ' * 4
38 def color_for_branch(branch, branch_hash, cur_hash, tag_set): 42
39 if branch.startswith('origin'): 43
40 color = Fore.RED 44 class OutputManager(object):
41 elif branch == NO_UPSTREAM or branch in tag_set: 45 """A class that manages a number of OutputLines and formats them into
42 color = Fore.MAGENTA 46 aligned columns."""
Matt Giuca 2014/09/01 05:12:50 The first sentence is supposed to fit on one line.
calamity 2014/09/01 06:53:42 Done.
43 elif branch_hash == cur_hash: 47
44 color = Fore.CYAN 48 def __init__(self):
45 else: 49 self.lines = []
46 color = Fore.GREEN 50 self.nocolor = False
47 51 self.max_column_lengths = []
48 if branch_hash == cur_hash: 52 self.num_columns = None
49 color += Style.BRIGHT 53
50 else: 54 def append(self, line):
51 color += Style.NORMAL 55 # All lines must have the same number of columns.
52 56 if not self.num_columns:
53 return color 57 self.num_columns = len(line.columns)
54 58 self.max_column_lengths = [0] * self.num_columns
55 59 assert self.num_columns == len(line.columns)
56 def print_branch(cur, cur_hash, branch, branch_hashes, par_map, branch_map, 60
57 tag_set, depth=0): 61 if self.nocolor:
58 branch_hash = branch_hashes[branch] 62 line.colors = [''] * self.num_columns
59 63
60 color = color_for_branch(branch, branch_hash, cur_hash, tag_set) 64 self.lines.append(line)
61 65
62 suffix = '' 66 # Update maximum column lengths
Matt Giuca 2014/09/01 05:12:50 Full stop.
calamity 2014/09/01 06:53:42 Done.
63 if cur == 'HEAD': 67 for i, col in enumerate(line.columns):
64 if branch_hash == cur_hash: 68 self.max_column_lengths[i] = max(self.max_column_lengths[i], len(col))
69
70 def __str__(self):
Matt Giuca 2014/09/01 05:12:51 I wouldn't use __str__ for this. __str__ should be
calamity 2014/09/01 06:53:42 Done.
71 return '\n'.join(
72 l.as_padded_string(self.max_column_lengths) for l in self.lines)
73
74
75 class OutputLine(object):
76 """A single line of data, consisting of an equal number of columns, colors and
77 separators."""
Matt Giuca 2014/09/01 05:12:50 Same.
calamity 2014/09/01 06:53:42 Done.
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
104 class BranchMapper(object):
105 """A class which constructs output representing the tree's branch structure"""
Matt Giuca 2014/09/01 05:12:51 nit: Need a full stop at the end. Also it needs t
calamity 2014/09/01 06:53:42 Done.
106
107 def __init__(self):
108 self.verbosity = 0
109 self.output = OutputManager()
110 self.tracking_info = get_all_tracking_info()
111 self.__gone_branches = set()
112
113 # A map of parents to a list of their children.
114 self.parent_map = collections.defaultdict(list)
115 for branch in self.tracking_info.keys():
Matt Giuca 2014/09/01 05:12:51 nit: Don't need .keys() (just "for branch in self.
calamity 2014/09/01 06:53:42 Done.
116 parent = self.tracking_info[branch].get('upstream', None)
117 if not parent:
118 parent = NO_UPSTREAM
119 elif parent not in self.tracking_info:
120 branch_upstream = upstream(branch)
121 # If git can't find the upstream, mark the upstream as gone.
122 if branch_upstream:
123 parent = branch_upstream
124 else:
125 self.__gone_branches.add(parent)
126 self.parent_map[parent].append(branch)
127
128 self.current_branch = current_branch()
129 self.current_hash = self.tracking_info[self.current_branch]['hash']
130
131 def start(self):
132 tag_set = tags()
133
134 while self.parent_map:
135 for parent in sorted(self.parent_map.keys()):
Matt Giuca 2014/09/01 05:12:50 Don't need .keys(). (Note that iterating over a d
calamity 2014/09/01 06:53:42 Done.
136 if parent in self.tracking_info:
137 continue
138
139 self.tracking_info[parent] = {
140 'hash': '' if self.__is_invalid_parent(parent)
141 else hash_one(parent, short=True)}
Matt Giuca 2014/09/01 05:12:50 Super hard to read. Break this expression out and
calamity 2014/09/01 06:53:42 N/A. But I fixed this to be a lot nicer =D
142
143 self.__append_branch(parent, tag_set)
144 break
145
146 def __is_invalid_parent(self, parent):
147 return parent == NO_UPSTREAM or parent in self.__gone_branches
148
149 def __color_for_branch(self, branch, branch_hash, tag_set):
150 if branch.startswith('origin'):
151 color = Fore.RED
152 elif self.__is_invalid_parent(branch) or branch in tag_set:
153 color = Fore.MAGENTA
154 elif branch_hash == self.current_hash:
155 color = Fore.CYAN
156 else:
157 color = Fore.GREEN
158
159 if branch_hash == self.current_hash:
160 color += Style.BRIGHT
161 else:
162 color += Style.NORMAL
163
164 return color
165
166 def __append_branch(self, branch, tag_set, depth=0):
167 """Recurses through the tree structure and appends an OutputLine to the
168 OutputManager for each branch."""
169 branch_hash = self.tracking_info[branch]['hash']
170
171 line = OutputLine()
172
173 # The branch name with appropriate indentation.
174 suffix = ''
175 if self.current_branch == 'HEAD':
176 if branch_hash == self.current_hash:
177 suffix = ' *'
178 elif branch == self.current_branch:
65 suffix = ' *' 179 suffix = ' *'
Matt Giuca 2014/09/01 05:12:50 optional (might look weird): Combine this into one
calamity 2014/09/01 06:53:42 Done.
66 elif branch == cur: 180 branch_string = (
67 suffix = ' *' 181 '{%s:GONE}' % branch if branch in self.__gone_branches else branch)
68 182 main_string = ' ' * depth + branch_string + suffix
69 print color + " "*depth + branch + suffix 183 line.append(
70 for child in par_map.pop(branch, ()): 184 main_string,
71 print_branch(cur, cur_hash, child, branch_hashes, par_map, branch_map, 185 color=self.__color_for_branch(branch, branch_hash, tag_set))
72 tag_set, depth=depth+1) 186
187 # The branch hash.
188 if self.verbosity >= 2:
189 line.append(branch_hash, 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 in self.tracking_info and not self.__is_invalid_parent(
199 self.tracking_info[branch].get('upstream', NO_UPSTREAM)):
200 ahead_string = self.tracking_info[branch].get('ahead', '')
201 behind_string = self.tracking_info[branch].get('behind', '')
202
203 if ahead_string or behind_string:
204 front_separator = '['
205 back_separator = ']'
206
207 if ahead_string and behind_string:
208 center_separator = '|'
209
210 line.append(front_separator, separator=' ')
211 line.append(ahead_string, separator=' ', color=Fore.MAGENTA)
212 line.append(center_separator, separator=' ')
213 line.append(behind_string, separator=' ', color=Fore.MAGENTA)
214 line.append(back_separator)
215
216 # The rietveld issue associated with the branch.
Matt Giuca 2014/09/01 05:12:51 Capital R.
calamity 2014/09/01 06:53:42 Done.
217 if self.verbosity >= 2:
218 none_text = '' if self.__is_invalid_parent(branch) else 'None'
219 url = git_cl.Changelist(branchref=branch).GetIssueURL()
220 line.append(url or none_text, color=Fore.BLUE if url else Fore.WHITE)
221
222 self.output.append(line)
223
224 for child in sorted(self.parent_map.pop(branch, ())):
225 self.__append_branch(child, tag_set, depth=depth + 1)
73 226
74 227
75 def main(argv): 228 def main(argv):
76 colorama.init() 229 colorama.init()
77 assert len(argv) == 1, "No arguments expected" 230 if normalized_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
78 branch_map = {} 231 print >> sys.stderr, (
79 par_map = collections.defaultdict(list) 232 'This tool will not show all tracking information for git version '
80 for branch in branches(): 233 'earlier than ' +
81 par = upstream(branch) or NO_UPSTREAM 234 '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
82 branch_map[branch] = par 235 '. Please consider upgrading.')
83 par_map[par].append(branch) 236
84 237 parser = argparse.ArgumentParser(
85 current = current_branch() 238 description='Print a a tree of all branches parented by their upstreams')
86 hashes = hash_multi(current, *branch_map.keys()) 239 parser.add_argument('-v', action='store_true',
iannucci 2014/08/29 18:41:47 let's use the action 'count' for this, instead of
calamity 2014/09/01 06:53:42 Done.
87 current_hash = hashes[0] 240 help='Display upstream tracking status')
iannucci 2014/08/29 18:41:47 should we just do this by default? is there any re
calamity 2014/09/01 06:53:42 Eh. Not really. I just didn't want to modify the o
Matt Giuca 2014/09/02 23:43:33 Seems consistent with git branch's behaviour of no
calamity 2014/09/03 01:43:21 Done.
88 par_hashes = {k: hashes[i+1] for i, k in enumerate(branch_map.iterkeys())} 241 parser.add_argument(
89 par_hashes[NO_UPSTREAM] = 0 242 '-vv',
90 tag_set = tags() 243 action='store_true',
91 while par_map: 244 help='Like -v but also show branch hash and Rietveld URL.')
92 for parent in par_map: 245 parser.add_argument('--no-color', action='store_true', dest='nocolor',
93 if parent not in branch_map: 246 help='Turn off colors.')
94 if parent not in par_hashes: 247
95 par_hashes[parent] = hash_one(parent) 248 opts = parser.parse_args(argv[1:])
96 print_branch(current, current_hash, parent, par_hashes, par_map, 249
97 branch_map, tag_set) 250 verbosity = 0
98 break 251 if opts.v:
99 252 verbosity = 1
253 if opts.vv:
254 verbosity = 2
iannucci 2014/08/29 18:41:47 then you don't need this
calamity 2014/09/01 06:53:42 Done.
255
256 mapper = BranchMapper()
257 mapper.verbosity = verbosity
258 mapper.output.nocolor = opts.nocolor
259 mapper.start()
260 print mapper.output
iannucci 2014/08/29 18:41:47 not sure if this is clearer than just returning th
calamity 2014/09/01 06:53:42 I think this is useful in case BranchMapper gets u
100 261
101 if __name__ == '__main__': 262 if __name__ == '__main__':
102 sys.exit(main(sys.argv)) 263 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