Commit 8bc9b5c1 authored by iannucci@chromium.org's avatar iannucci@chromium.org

Add git-map and git-map-branches to depot_tools.

git-map: Show your local repo's history in a pseudo-graphical format from the command line.

git-map-branches: Show the topology of all of your branches, and their upstream relationships.

git-nav-upstream: Navigate (checkout) to the upstream branch of the current branch.

git-nav-downstream: Navigate (checkout) to a downstream branch of the current branch. If there's more than one downstream branch, then present a menu to select which one you want.

R=agable@chromium.org, hinoka@chromium.org, stip@chromium.org, szager@chromium.org
BUG=

Review URL: https://codereview.chromium.org/184113002

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@256384 0039d316-1c4b-4281-b951-d872f2087c98
parent ab4d438e
#!/bin/bash
# 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.
# git_map.py -- a git-command for presenting a graphical view of the git
# history.
. $(type -P python_git_runner.sh) | less -R
\ No newline at end of file
#!/bin/bash
# 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.
# git_map_branches.py -- a git-command for presenting a graphical view of git
# branches in the current repo, and their relationships to each other.
. $(type -P python_git_runner.sh)
#!/bin/bash
# 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.
# git_nav_downstream.py -- a git-command to navigate to a downstream branch.
. $(type -P python_git_runner.sh)
#!/bin/bash
# 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.
# a git-command to navigate to the upstream branch.
git checkout '@{u}'
......@@ -199,6 +199,25 @@ class ProgressPrinter(object):
del self._thread
def branches(*args):
NO_BRANCH = ('* (no branch)', '* (detached from ')
for line in run('branch', *args).splitlines():
if line.startswith(NO_BRANCH):
continue
yield line.split()[-1]
def config_list(option):
try:
return run('config', '--get-all', option).split()
except subprocess2.CalledProcessError:
return []
def current_branch():
return run('rev-parse', '--abbrev-ref', 'HEAD')
def parse_commitrefs(*commitrefs):
"""Returns binary encoded commit hashes for one or more commitrefs.
......@@ -208,7 +227,7 @@ def parse_commitrefs(*commitrefs):
* 'cool_branch~2'
"""
try:
return map(binascii.unhexlify, hashes(*commitrefs))
return map(binascii.unhexlify, hash_multi(*commitrefs))
except subprocess2.CalledProcessError:
raise BadCommitRefException(commitrefs)
......@@ -231,7 +250,11 @@ def run(*cmd, **kwargs):
return ret
def hashes(*reflike):
def hash_one(reflike):
return run('rev-parse', reflike)
def hash_multi(*reflike):
return run('rev-parse', *reflike).splitlines()
......@@ -249,6 +272,10 @@ def intern_f(f, kind='blob'):
return ret
def tags(*args):
return run('tag', *args).splitlines()
def tree(treeref, recurse=False):
"""Returns a dict representation of a git tree object.
......@@ -286,6 +313,14 @@ def tree(treeref, recurse=False):
return ret
def upstream(branch):
try:
return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
branch+'@{upstream}')
except subprocess2.CalledProcessError:
return None
def mktree(treedict):
"""Makes a git tree object and returns its hash.
......
#!/usr/bin/env python
"""
Provides an augmented `git log --graph` view. In particular, it also annotates
commits with branches + tags that point to them. Items are colorized as follows:
* Cyan - Currently checked out branch
* Green - Local branch
* Red - Remote branches
* Magenta - Tags
* Blue background - The currently checked out commit
"""
import sys
import subprocess2
from git_common import current_branch, branches, tags, config_list, GIT_EXE
from third_party import colorama
CYAN = colorama.Fore.CYAN
GREEN = colorama.Fore.GREEN
MAGENTA = colorama.Fore.MAGENTA
RED = colorama.Fore.RED
BLUEBAK = colorama.Back.BLUE
BRIGHT = colorama.Style.BRIGHT
RESET = colorama.Fore.RESET + colorama.Back.RESET + colorama.Style.RESET_ALL
def main():
map_extra = config_list('depot_tools.map_extra')
fmt = '%C(red bold)%h%x09%Creset%C(green)%d%Creset %C(yellow)%ad%Creset ~ %s'
log_proc = subprocess2.Popen(
[GIT_EXE, 'log', '--graph', '--full-history', '--branches', '--tags',
'--remotes', '--color=always', '--date=short', ('--pretty=format:' + fmt)
] + map_extra + sys.argv[1:],
stdout=subprocess2.PIPE,
shell=False)
current = current_branch()
all_branches = set(branches())
if current in all_branches:
all_branches.remove(current)
all_tags = set(tags())
try:
for line in log_proc.stdout.xreadlines():
start = line.find(GREEN+' (')
end = line.find(')', start)
if start != -1 and end != -1:
start += len(GREEN) + 2
branch_list = line[start:end].split(', ')
branches_str = ''
if branch_list:
colored_branches = []
head_marker = ''
for b in branch_list:
if b == "HEAD":
head_marker = BLUEBAK+BRIGHT+'*'
continue
if b == current:
colored_branches.append(CYAN+BRIGHT+b+RESET)
current = None
elif b in all_branches:
colored_branches.append(GREEN+BRIGHT+b+RESET)
all_branches.remove(b)
elif b in all_tags:
colored_branches.append(MAGENTA+BRIGHT+b+RESET)
elif b.startswith('tag: '):
colored_branches.append(MAGENTA+BRIGHT+b[5:]+RESET)
else:
colored_branches.append(RED+b)
branches_str = '(%s) ' % ((GREEN+", ").join(colored_branches)+GREEN)
line = "%s%s%s" % (line[:start-1], branches_str, line[end+5:])
if head_marker:
line = line.replace('*', head_marker, 1)
sys.stdout.write(line)
except (IOError, KeyboardInterrupt):
pass
finally:
sys.stderr.close()
sys.stdout.close()
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
"""
Provides a short mapping of all the branches in your local repo, organized by
their upstream ('tracking branch') layout. Example:
origin/master
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
* Magenta - a placeholder for the '{NO UPSTREAM}' "branch". If you have
local branches which do not track any upstream, then you will see this.
"""
import collections
import sys
from third_party import colorama
from third_party.colorama import Fore, Style
from git_common import current_branch, branches, upstream, hash_one, hash_multi
NO_UPSTREAM = '{NO UPSTREAM}'
def print_branch(cur, cur_hash, branch, branch_hashes, par_map, branch_map,
depth=0):
branch_hash = branch_hashes[branch]
if branch.startswith('origin'):
color = Fore.RED
elif branch == NO_UPSTREAM:
color = Fore.MAGENTA
elif branch_hash == cur_hash:
color = Fore.CYAN
else:
color = Fore.GREEN
if branch_hash == cur_hash:
color += Style.BRIGHT
else:
color += Style.NORMAL
print color + " "*depth + branch + (" *" if branch == cur else "")
for child in par_map.pop(branch, ()):
print_branch(cur, cur_hash, child, branch_hashes, par_map, branch_map,
depth=depth+1)
def main(argv):
colorama.init()
assert len(argv) == 1, "No arguments expected"
branch_map = {}
par_map = collections.defaultdict(list)
for branch in branches():
par = upstream(branch) or NO_UPSTREAM
branch_map[branch] = par
par_map[par].append(branch)
current = current_branch()
hashes = hash_multi(current, *branch_map.keys())
current_hash = hashes[0]
par_hashes = {k: hashes[i+1] for i, k in enumerate(branch_map.iterkeys())}
par_hashes[NO_UPSTREAM] = 0
while par_map:
for parent in par_map:
if parent not in branch_map:
if parent not in par_hashes:
par_hashes[parent] = hash_one(parent)
print_branch(current, current_hash, parent, par_hashes, par_map,
branch_map)
break
if __name__ == '__main__':
sys.exit(main(sys.argv))
#!/usr/bin/env python
"""
Checks out a downstream branch from the currently checked out branch. If there
is more than one downstream branch, then this script will prompt you to select
which branch.
"""
import sys
from git_common import current_branch, branches, upstream, run, hash_one
def main(argv):
assert len(argv) == 1, "No arguments expected"
upfn = upstream
cur = current_branch()
if cur == 'HEAD':
upfn = lambda b: hash_one(upstream(b))
cur = hash_one(cur)
downstreams = [b for b in branches() if upfn(b) == cur]
if not downstreams:
return "No downstream branches"
elif len(downstreams) == 1:
run('checkout', downstreams[0])
else:
high = len(downstreams) - 1
print
while True:
print "Please select a downstream branch"
for i, b in enumerate(downstreams):
print " %d. %s" % (i, b)
r = raw_input("Selection (0-%d)[0]: " % high).strip() or '0'
if not r.isdigit() or (0 > int(r) > high):
print "Invalid choice."
else:
run('checkout', downstreams[int(r)])
break
if __name__ == '__main__':
sys.exit(main(sys.argv))
......@@ -153,7 +153,7 @@ def finalize(targets):
assert updater.returncode == 0
tree_id = git.run('write-tree', env=env)
commit_cmd = ['commit-tree', '-m', msg, '-p'] + git.hashes(REF)
commit_cmd = ['commit-tree', '-m', msg, '-p'] + git.hash_multi(REF)
for t in targets:
commit_cmd.extend(['-p', binascii.hexlify(t)])
commit_cmd.append(tree_id)
......
......@@ -256,6 +256,7 @@ class GitRepo(object):
self.git('init')
for commit in schema.walk():
self._add_schema_commit(commit, schema.data_for(commit.name))
self.last_commit = self[commit.name]
if schema.master:
self.git('update-ref', 'master', self[schema.master])
......@@ -321,7 +322,7 @@ class GitRepo(object):
self.commit_map[commit.name] = self.git('rev-parse', 'HEAD').stdout.strip()
self.git('tag', 'tag_%s' % commit.name, self[commit.name])
if commit.is_branch:
self.git('update-ref', 'branch_%s' % commit.name, self[commit.name])
self.git('branch', '-f', 'branch_%s' % commit.name, self[commit.name])
def git(self, *args, **kwargs):
"""Runs a git command specified by |args| in this repo."""
......@@ -394,6 +395,9 @@ class GitRepoReadOnlyTestBase(GitRepoSchemaTestBase):
assert cls.REPO is not None
cls.repo = cls.r_schema.reify()
def setUp(self):
self.repo.git('checkout', '-f', self.repo.last_commit)
@classmethod
def tearDownClass(cls):
cls.repo.nuke()
......
......@@ -170,7 +170,7 @@ class GitReadOnlyFunctionsTest(git_test_utils.GitRepoReadOnlyTestBase,
def testHashes(self):
ret = self.repo.run(
self.gc.hashes, *[
self.gc.hash_multi, *[
'master',
'master~3',
self.repo['E']+'~',
......@@ -185,6 +185,22 @@ class GitReadOnlyFunctionsTest(git_test_utils.GitRepoReadOnlyTestBase,
self.repo['E'],
self.repo['C'],
], ret)
self.assertEquals(
self.repo.run(self.gc.hash_one, 'branch_D'),
self.repo['D']
)
def testCurrentBranch(self):
self.repo.git('checkout', 'branch_D')
self.assertEqual(self.repo.run(self.gc.current_branch), 'branch_D')
def testBranches(self):
self.assertEqual(self.repo.run(set, self.gc.branches()),
set(('branch_D', 'root_A')))
def testTags(self):
self.assertEqual(set(self.repo.run(self.gc.tags)),
{'tag_'+l for l in 'ABCDE'})
def testParseCommitrefs(self):
ret = self.repo.run(
......@@ -274,6 +290,24 @@ class GitMutableFunctionsTest(git_test_utils.GitRepoReadWriteTestBase,
tree_hash = self.repo.run(self.gc.mktree, tree)
self.assertEquals('37b61866d6e061c4ba478e7eb525be7b5752737d', tree_hash)
def testConfig(self):
self.repo.git('config', '--add', 'happy.derpies', 'food')
self.assertEquals(self.repo.run(self.gc.config_list, 'happy.derpies'),
['food'])
self.assertEquals(self.repo.run(self.gc.config_list, 'sad.derpies'), [])
self.repo.git('config', '--add', 'happy.derpies', 'cat')
self.assertEquals(self.repo.run(self.gc.config_list, 'happy.derpies'),
['food', 'cat'])
def testUpstream(self):
self.repo.git('commit', '--allow-empty', '-am', 'foooooo')
self.assertEquals(self.repo.run(self.gc.upstream, 'bobly'), None)
self.assertEquals(self.repo.run(self.gc.upstream, 'master'), None)
self.repo.git('checkout', '-tb', 'happybranch', 'master')
self.assertEquals(self.repo.run(self.gc.upstream, 'happybranch'),
'master')
if __name__ == '__main__':
sys.exit(coverage_utils.covered_main(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment