123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- #!/usr/bin/env python3
- # Copyright 2014 The Chromium Authors. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """Print dependency tree of branches in local repo.
- Example:
- origin/main
- cool_feature
- dependent_feature
- other_dependent_feature
- other_feature
- Branches are colorized as follows:
- * Red - a remote branch (usually the root of all local branches)
- * Cyan - a local branch which is the same as HEAD
- * Note that multiple branches may be Cyan, if they are all on the same
- commit, and you have that commit checked out.
- * Green - a local branch
- * Blue - a 'branch-heads' branch
- * Magenta - a tag
- * Magenta '{NO UPSTREAM}' - If you have local branches which do not track any
- upstream, then you will see this.
- """
- from __future__ import print_function
- import argparse
- import collections
- import metrics
- import os
- import subprocess2
- import sys
- from git_common import current_branch, upstream, tags, get_branches_info
- from git_common import get_git_version, MIN_UPSTREAM_TRACK_GIT_VERSION, hash_one
- from git_common import run
- import setup_color
- from third_party.colorama import Fore, Style
- DEFAULT_SEPARATOR = ' ' * 4
- class OutputManager(object):
- """Manages a number of OutputLines and formats them into aligned columns."""
- def __init__(self):
- self.lines = []
- self.nocolor = False
- self.max_column_lengths = []
- self.num_columns = None
- def append(self, line):
- # All lines must have the same number of columns.
- if not self.num_columns:
- self.num_columns = len(line.columns)
- self.max_column_lengths = [0] * self.num_columns
- assert self.num_columns == len(line.columns)
- if self.nocolor:
- line.colors = [''] * self.num_columns
- self.lines.append(line)
- # Update maximum column lengths.
- for i, col in enumerate(line.columns):
- self.max_column_lengths[i] = max(self.max_column_lengths[i], len(col))
- def as_formatted_string(self):
- return '\n'.join(
- l.as_padded_string(self.max_column_lengths) for l in self.lines)
- class OutputLine(object):
- """A single line of data.
- This consists of an equal number of columns, colors and separators."""
- def __init__(self):
- self.columns = []
- self.separators = []
- self.colors = []
- def append(self, data, separator=DEFAULT_SEPARATOR, color=Fore.WHITE):
- self.columns.append(data)
- self.separators.append(separator)
- self.colors.append(color)
- def as_padded_string(self, max_column_lengths):
- """"Returns the data as a string with each column padded to
- |max_column_lengths|."""
- output_string = ''
- for i, (color, data, separator) in enumerate(
- zip(self.colors, self.columns, self.separators)):
- if max_column_lengths[i] == 0:
- continue
- padding = (max_column_lengths[i] - len(data)) * ' '
- output_string += color + data + padding + separator
- return output_string.rstrip()
- class BranchMapper(object):
- """A class which constructs output representing the tree's branch structure.
- Attributes:
- __branches_info: a map of branches to their BranchesInfo objects which
- consist of the branch hash, upstream and ahead/behind status.
- __gone_branches: a set of upstreams which are not fetchable by git"""
- def __init__(self):
- self.verbosity = 0
- self.maxjobs = 0
- self.show_subject = False
- self.output = OutputManager()
- self.__gone_branches = set()
- self.__branches_info = None
- self.__parent_map = collections.defaultdict(list)
- self.__current_branch = None
- self.__current_hash = None
- self.__tag_set = None
- self.__status_info = {}
- def start(self):
- self.__branches_info = get_branches_info(
- include_tracking_status=self.verbosity >= 1)
- if (self.verbosity >= 2):
- # Avoid heavy import unless necessary.
- from git_cl import get_cl_statuses, color_for_status, Changelist
- change_cls = [Changelist(branchref='refs/heads/'+b)
- for b in self.__branches_info.keys() if b]
- status_info = get_cl_statuses(change_cls,
- fine_grained=self.verbosity > 2,
- max_processes=self.maxjobs)
- # This is a blocking get which waits for the remote CL status to be
- # retrieved.
- for cl, status in status_info:
- self.__status_info[cl.GetBranch()] = (cl.GetIssueURL(short=True),
- color_for_status(status), status)
- roots = set()
- # A map of parents to a list of their children.
- for branch, branch_info in self.__branches_info.items():
- if not branch_info:
- continue
- parent = branch_info.upstream
- if self.__check_cycle(branch):
- continue
- if not self.__branches_info[parent]:
- branch_upstream = upstream(branch)
- # If git can't find the upstream, mark the upstream as gone.
- if branch_upstream:
- parent = branch_upstream
- else:
- self.__gone_branches.add(parent)
- # A parent that isn't in the branches info is a root.
- roots.add(parent)
- self.__parent_map[parent].append(branch)
- self.__current_branch = current_branch()
- self.__current_hash = hash_one('HEAD', short=True)
- self.__tag_set = tags()
- if roots:
- for root in sorted(roots):
- self.__append_branch(root)
- else:
- no_branches = OutputLine()
- no_branches.append('No User Branches')
- self.output.append(no_branches)
- def __check_cycle(self, branch):
- # Maximum length of the cycle is `num_branches`. This limit avoids running
- # into a cycle which does *not* contain `branch`.
- num_branches = len(self.__branches_info)
- cycle = [branch]
- while len(cycle) < num_branches and self.__branches_info[cycle[-1]]:
- parent = self.__branches_info[cycle[-1]].upstream
- cycle.append(parent)
- if parent == branch:
- print('Warning: Detected cycle in branches: {}'.format(
- ' -> '.join(cycle)), file=sys.stderr)
- return True
- return False
- def __is_invalid_parent(self, parent):
- return not parent or parent in self.__gone_branches
- def __color_for_branch(self, branch, branch_hash):
- if branch.startswith('origin/'):
- color = Fore.RED
- elif branch.startswith('branch-heads'):
- color = Fore.BLUE
- elif self.__is_invalid_parent(branch) or branch in self.__tag_set:
- color = Fore.MAGENTA
- elif self.__current_hash.startswith(branch_hash):
- color = Fore.CYAN
- else:
- color = Fore.GREEN
- if branch_hash and self.__current_hash.startswith(branch_hash):
- color += Style.BRIGHT
- else:
- color += Style.NORMAL
- return color
- def __append_branch(self, branch, depth=0):
- """Recurses through the tree structure and appends an OutputLine to the
- OutputManager for each branch."""
- branch_info = self.__branches_info[branch]
- if branch_info:
- branch_hash = branch_info.hash
- else:
- try:
- branch_hash = hash_one(branch, short=True)
- except subprocess2.CalledProcessError:
- branch_hash = None
- line = OutputLine()
- # The branch name with appropriate indentation.
- suffix = ''
- if branch == self.__current_branch or (
- self.__current_branch == 'HEAD' and branch == self.__current_hash):
- suffix = ' *'
- branch_string = branch
- if branch in self.__gone_branches:
- branch_string = '{%s:GONE}' % branch
- if not branch:
- branch_string = '{NO_UPSTREAM}'
- main_string = ' ' * depth + branch_string + suffix
- line.append(
- main_string,
- color=self.__color_for_branch(branch, branch_hash))
- # The branch hash.
- if self.verbosity >= 2:
- line.append(branch_hash or '', separator=' ', color=Fore.RED)
- # The branch tracking status.
- if self.verbosity >= 1:
- commits_string = ''
- behind_string = ''
- front_separator = ''
- center_separator = ''
- back_separator = ''
- if branch_info and not self.__is_invalid_parent(branch_info.upstream):
- behind = branch_info.behind
- commits = branch_info.commits
- if commits:
- commits_string = '%d commit' % commits
- commits_string += 's' if commits > 1 else ' '
- if behind:
- behind_string = 'behind %d' % behind
- if commits or behind:
- front_separator = '['
- back_separator = ']'
- if commits and behind:
- center_separator = '|'
- line.append(front_separator, separator=' ')
- line.append(commits_string, separator=' ', color=Fore.MAGENTA)
- line.append(center_separator, separator=' ')
- line.append(behind_string, separator=' ', color=Fore.MAGENTA)
- line.append(back_separator)
- # The Rietveld issue associated with the branch.
- if self.verbosity >= 2:
- (url, color, status) = ('', '', '') if self.__is_invalid_parent(branch) \
- else self.__status_info[branch]
- if self.verbosity > 2:
- line.append('{} ({})'.format(url, status) if url else '', color=color)
- else:
- line.append(url or '', color=color)
- # The subject of the most recent commit on the branch.
- if self.show_subject:
- if not self.__is_invalid_parent(branch):
- line.append(run('log', '-n1', '--format=%s', branch, '--'))
- else:
- line.append('')
- self.output.append(line)
- for child in sorted(self.__parent_map.pop(branch, ())):
- self.__append_branch(child, depth=depth + 1)
- def print_desc():
- for line in __doc__.splitlines():
- starpos = line.find('* ')
- if starpos == -1 or '-' not in line:
- print(line)
- else:
- _, color, rest = line.split(None, 2)
- outline = line[:starpos+1]
- outline += getattr(Fore, color.upper()) + " " + color + " " + Fore.RESET
- outline += rest
- print(outline)
- print('')
- @metrics.collector.collect_metrics('git map-branches')
- def main(argv):
- setup_color.init()
- if get_git_version() < MIN_UPSTREAM_TRACK_GIT_VERSION:
- print(
- 'This tool will not show all tracking information for git version '
- 'earlier than ' +
- '.'.join(str(x) for x in MIN_UPSTREAM_TRACK_GIT_VERSION) +
- '. Please consider upgrading.', file=sys.stderr)
- if '-h' in argv:
- print_desc()
- parser = argparse.ArgumentParser()
- parser.add_argument('-v', action='count', default=0,
- help=('Pass once to show tracking info, '
- 'twice for hash and review url, '
- 'thrice for review status'))
- parser.add_argument('--no-color', action='store_true', dest='nocolor',
- help='Turn off colors.')
- parser.add_argument(
- '-j', '--maxjobs', action='store', type=int,
- help='The number of jobs to use when retrieving review status')
- parser.add_argument('--show-subject', action='store_true',
- dest='show_subject', help='Show the commit subject.')
- opts = parser.parse_args(argv)
- mapper = BranchMapper()
- mapper.verbosity = opts.v
- mapper.output.nocolor = opts.nocolor
- mapper.maxjobs = opts.maxjobs
- mapper.show_subject = opts.show_subject
- mapper.start()
- print(mapper.output.as_formatted_string())
- return 0
- if __name__ == '__main__':
- try:
- with metrics.collector.print_notice_and_exit():
- sys.exit(main(sys.argv[1:]))
- except KeyboardInterrupt:
- sys.stderr.write('interrupted\n')
- sys.exit(1)
|