| #!/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() |