blob: 5583aac797b33e0a3167ebe510ed7bf58aa51874 [file] [log] [blame]
#!/usr/bin/python
#
# Copyright (c) 2013 The Chromium OS 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 script to diff master branch and factory branch.
It shows the commits that only in master branch or only in factory branch.
By default, these repos are diff'ed:
platform/factory
platform/chromeos-hwid
If a board is specified, the private overlay repo for that board is also
included. In this case, if branch is not specified, this script tries to find
the latest factory-<board>-*.B branch.
"""
import argparse
from collections import namedtuple
import glob
import logging
import os
import re
import subprocess
import sys
import factory_common # pylint: disable=W0611
from cros.factory.utils.process_utils import CheckOutput, Spawn
from cros.factory.tools import build_board
DiffEntry = namedtuple('DiffEntry',
['left_right', 'hash', 'author', 'subject'])
COLOR_RESET = '\033[0m'
COLOR_YELLOW = '\033[33m'
COLOR_CYAN = '\033[36m'
COLOR_GREEN = '\033[1;32m'
CHERRY_PICK = 'CHERRY-PICK: '
PRIVATE_OVERLAY_LIST = ['src/private-overlays/overlay-%s-private',
'src/private-overlays/overlay-variant-*-%s-private']
FACTORY_REPO_LIST = ['src/platform/factory', 'src/platform/chromeos-hwid']
# Please add the repo if you think it is important in factory.
OTHER_REPO_LIST = ['src/platform/touch_updater', 'src/platform/mosys',
'src/platform/factory_installer', 'src/platform/ec',
'src/third_party/autotest/files',
'src/third_party/xf86-video-armsoc', 'src/third_party/adhd',
'src/third_party/chromiumos-overlay']
# The kernel repo has several different versions. The actual kernel version will
# be determined during runtime based on the board name provided.
KERNEL_REPO_PATTERN = 'src/third_party/kernel/v%(version)s'
# m/master does not point to cros/master in the repo in this list. We can only
# know where it points to if the tree was inited without -b and has
# m/master branch.
DIFFERENT_MASTER_REPO_LIST = glob.glob('src/third_party/kernel/*')
# Repo in this list like 'src/third_party/chromiumos-overlay' does not contain
# other branches by default. 'git fetch cros' can fetch all the branches.
# 'git fetch cros <factory_branch>' can fetch specified factory branch.
# However, repo sync will not update those fetched branches for you,
# so we have to force fetching the branch each time we want to diff.
FORCE_FETCH_REPO_LIST = ['src/third_party/chromiumos-overlay']
SRC = os.path.join(os.environ['CROS_WORKON_SRCROOT'], 'src')
def GetDefaultBoardOrNone():
try:
return (open(os.path.join(SRC, 'scripts', '.default_board')).read()
.strip().rpartition('_')[2])
except IOError:
return None
def GetFullRepoPath(repo_path):
"""Returns the full path of the given repo."""
return os.path.join(os.path.expanduser('~'), 'trunk', repo_path)
def FindGitPrefix(repo_path):
"""Gets Git prefix, either 'cros' or 'cros-internal'."""
os.chdir(GetFullRepoPath(repo_path))
branch_list = CheckOutput(['git', 'branch', '-av'])
for line in reversed(branch_list.split('\n')):
match = re.search(r'remotes\/([^/]*)/[^/]*', line)
# Look for remote branch starting with 'cros' or 'cros-internal'.
if match and match.group(1).startswith('cros'):
return match.group(1)
return None
def GetBranch(board):
"""Gets latest factory branch for a board."""
if not board:
return None
os.chdir(GetFullRepoPath('src/platform/factory'))
branch_list = CheckOutput(['git', 'branch', '-av'])
for line in reversed(branch_list.split('\n')):
match = re.search(r'remotes\/cros\/(factory-%s-\d+.B)' % board, line)
if match:
return match.group(1)
return None
def GetPrivateOverlay(board):
"""Gets the path to private overlay."""
for pattern in PRIVATE_OVERLAY_LIST:
path = glob.glob(GetFullRepoPath(pattern % board))
if path:
if len(path) > 1:
logging.warning('Found more than one private overlays:\n%s',
'\n'.join(path))
return path[0]
return None
def GetBoardKernelVersion(board):
"""Gets the kernel version used by the given board."""
KERNEL_SRC_USE_RE = re.compile(r'\+kernel-(\d+_\d+)', re.MULTILINE)
return KERNEL_SRC_USE_RE.search(
CheckOutput(['equery-%s' % board, 'uses', 'linux-sources'])
).group(1).replace('_', '.')
def GetDiffList(diff):
"""Generates a list of DiffEntry from Git output.
Args:
diff: The output from Git log command.
Returns:
A list of DiffEntry. Each DiffEntry corresponds to a commit that
is in one branch.
"""
ret = []
for line in diff.split('\n'):
match = re.match(r'([<>]) ([0-9a-f]+) \(([^\)]+)\) (.*)', line)
if match:
if match.group(4) == 'Marking set of ebuilds as stable':
continue
ret.append(DiffEntry(*match.groups()))
return ret
def RemoveCherryPickPrefix(subject):
return (subject[len(CHERRY_PICK):]
if subject.startswith(CHERRY_PICK)
else subject)
def RemoveCherryPick(diff_list):
diffed = {'<': set(), '>': set()}
for entry in diff_list:
subject = RemoveCherryPickPrefix(entry.subject)
diffed[entry.left_right].add((entry.author, subject))
# If a commit shows up in both branches, it is cherry-picked.
# (Even though Git doesn't know they are the same.)
cherrypicked = diffed['<'] & diffed['>']
return [entry for entry in diff_list
if (entry.author, RemoveCherryPickPrefix(entry.subject))
not in cherrypicked]
def RefExist(repo_path, full_branch_name):
"""Checks if there exists a reference named full_branch_name."""
os.chdir(GetFullRepoPath(repo_path))
cmd = ['git', 'show-ref', '--verify',
'refs/remotes/%s' % full_branch_name]
try:
Spawn(cmd, check_call=True, ignore_stdout=True, ignore_stderr=True)
except subprocess.CalledProcessError:
logging.warning('ref %s does not exist in %s', full_branch_name, repo_path)
return False
return True
def BranchExist(repo_path, full_branch_name):
"""Checks if there exists a branch named full_branc_name."""
os.chdir(GetFullRepoPath(repo_path))
branch_list = CheckOutput(['git', 'branch', '-av'])
for line in reversed(branch_list.split('\n')):
# Matches for <local_branch_name>
match = re.search(r'\s*(\S*)\s*', line)
if match and match.group(1) == full_branch_name:
return True
# Matches for remotes/<prefix>/<branch_name>
match = re.search(r'\s*remotes\/(\S*)\s*', line)
if match and match.group(1) == full_branch_name:
return True
logging.warning('branch %s does not exist in %s', full_branch_name, repo_path)
return False
def FetchBranch(repo_path, prefix, branch, local_branch_name):
"""Fetches reference prefix/branch to fetched ref named local_branch_name."""
logging.warning('Fetching branch %s/%s to %s for repo %s',
prefix, branch, local_branch_name, repo_path)
os.chdir(GetFullRepoPath(repo_path))
# Removes old branch.
if BranchExist(repo_path, local_branch_name):
cmd = ['git', 'branch', '-D', local_branch_name]
Spawn(cmd, call=True)
cmd = ['git', 'fetch', prefix, '%s:%s' % (branch, local_branch_name)]
Spawn(cmd, check_call=True)
def DiffRepo(repo_path, args, init_with_master):
print '%s*** Diff %s ***%s' % (COLOR_GREEN, repo_path, COLOR_RESET)
os.chdir(GetFullRepoPath(repo_path))
prefix = FindGitPrefix(repo_path)
# If the tree is not init with master branch, we can only guess m/master is
# cros/master or cros-internal/master
master_branch_prefix = 'm' if init_with_master else prefix
master_branch = master_branch_prefix + '/master'
compare_branch = prefix + '/' + args.branch
if (repo_path == (KERNEL_REPO_PATTERN % dict(
version=GetBoardKernelVersion(args.board.full_name)))):
# The kernel repo has a slightly different branch naming. There is a
# '-chromeos-<kernel_version>' suffix after the branch name.
compare_branch += '-chromeos-%s' % GetBoardKernelVersion(
args.board.full_name)
# Force fetching for if repo is in FORCE_FETCH_REPO_LIST.
# When fetching for the branch, prefix can only be 'cros' or 'cros-internal'.
if repo_path in FORCE_FETCH_REPO_LIST:
FetchBranch(repo_path, prefix, 'master', 'TEMP_MASTER')
FetchBranch(repo_path, prefix, args.branch, 'TEMP_COMPARE')
master_branch, compare_branch = 'TEMP_MASTER', 'TEMP_COMPARE'
# Repo not in FORCE_FECH_REPO_LIST should contain both branches.
elif not (BranchExist(repo_path, master_branch) and
BranchExist(repo_path, compare_branch)):
logging.error('%s does not contain both %s and %s, you should add this '
'repo to FORCE_FETCH_REPO_LIST', repo_path, master_branch,
compare_branch)
cmd = ['git', 'log', '--cherry-pick', '--oneline', '--left-right',
'--pretty=format:%m %h (%an) %s',
'%s...%s' % (master_branch, compare_branch)]
if args.author:
cmd += ['--author', args.author]
diff = CheckOutput(cmd)
diff_list = GetDiffList(diff)
diff_list = RemoveCherryPick(diff_list)
# Show branch name. (e.g. [4131.B])
branch_name = '[%s]' % args.branch[-6:]
# To make branch stands out, we only show [------] for commits that
# are on ToT.
for entry in diff_list:
if args.factory_only and entry.left_right == '<':
continue
if args.master_only and entry.left_right == '>':
continue
print '%s%s %s%s %s %s(%s)%s' % (COLOR_YELLOW,
'[------]' if entry.left_right == '<'
else branch_name,
entry.hash,
COLOR_RESET,
entry.subject,
COLOR_CYAN,
entry.author,
COLOR_RESET)
print ''
def main():
parser = argparse.ArgumentParser(
description=('List the commits that are only in one of master '
'and factory branch.'))
parser.add_argument('--branch', '-r', default=None,
help='name of the factory branch')
parser.add_argument('--board', '-b', default=None,
help='board name')
parser.add_argument('--author', '-a', default=None,
help='Limit the output to this author only')
parser.add_argument('--factory_only', '-o', action='store_true',
help='Only show commits on factory branch')
parser.add_argument('--master_only', '-m', action='store_true',
help='Only show commits on ToT')
parser.add_argument('--show_other_repos', '-s', action='store_true',
help='Show commits in OTHER_REPO_LIST as well')
args = parser.parse_args()
args.board = args.board or GetDefaultBoardOrNone()
if args.board:
args.board = build_board.BuildBoard(args.board)
if not args.branch:
args.branch = GetBranch(args.board.short_name)
if not args.branch:
logging.error('Cannot determine factory branch name. '
'Specify --branch to continue.')
sys.exit(1)
repo_list = FACTORY_REPO_LIST
if args.show_other_repos:
repo_list += OTHER_REPO_LIST
# Add the active kernel repo of the give board into list.
repo_list += [KERNEL_REPO_PATTERN %
dict(version=GetBoardKernelVersion(args.board.full_name))]
if args.board:
repo_list.append(GetPrivateOverlay(args.board.short_name))
if RefExist('src/platform/factory', 'm/master'):
init_with_master = True
else:
logging.warning('This tree was inited with -b <branch_name> so there is '
'no clue where m/master might point to.')
logging.warning('Removing repo in %s from repo_list',
DIFFERENT_MASTER_REPO_LIST)
repo_list = [x for x in repo_list if x not in DIFFERENT_MASTER_REPO_LIST]
init_with_master = False
if args.factory_only and args.master_only:
logging.warning('Both --factory_only and --master_only specified. '
'Ignoring both.')
args.factory_only = False
args.master_only = False
for repo in repo_list:
DiffRepo(repo, args, init_with_master)
sys.exit(0)
if __name__ == '__main__':
main()