webrtc/tools/autoroller/roll_webrtc_in_chromium.py
kjellander@webrtc.org dd0292a774 Send to CQ by default and add --no-commit flag + cleanup.
Send the created CL to the CQ right away upon creation.
Add a --no-commit flag that can be specified to avoid that.
Remove the trybot status feature (--status) and the commit CL
feature (--commit).

BUG=chromium:433305
TESTED=Ran:
tools/autoroller/roll_webrtc_in_chromium.py --chromium-checkout /ssd/chrome/src --dry-run --ignore-checks
and verified it didn't trigger tryjobs or sent to CQ. Then I ran:
tools/autoroller/roll_webrtc_in_chromium.py --chromium-checkout /ssd/chrome/src --ignore-checks
and verified it was sent to CQ.

R=tommi@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/42739004

Cr-Commit-Position: refs/heads/master@{#8715}
git-svn-id: http://webrtc.googlecode.com/svn/trunk@8715 4adac7df-926f-26a2-2b94-8c16560cd09d
2015-03-13 09:31:31 +00:00

383 lines
14 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
#
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE file in the root of the source
# tree. An additional intellectual property rights grant can be found
# in the file PATENTS. All contributing project authors may
# be found in the AUTHORS file in the root of the source tree.
import argparse
import collections
import getpass
import json
import logging
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
import urllib2
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
ROOT_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, os.pardir, os.pardir))
sys.path.insert(1, os.path.join(ROOT_DIR, 'tools'))
import find_depot_tools
find_depot_tools.add_depot_tools_to_path()
from gclient import GClientKeywords
from third_party import upload
# Avoid depot_tools/third_party/upload.py print verbose messages.
upload.verbosity = 0 # Errors only.
CHROMIUM_GIT_URL = 'https://chromium.googlesource.com/chromium/src.git'
GIT_SVN_ID_RE = re.compile('^Cr-Original-Commit-Position: .*#([0-9]+).*$')
CL_ISSUE_RE = re.compile('^Issue number: ([0-9]+) \((.*)\)$')
RIETVELD_URL_RE = re.compile('^https?://(.*)/(.*)')
ROLL_BRANCH_NAME = 'special_webrtc_roll_branch'
# Use a shell for subcommands on Windows to get a PATH search.
USE_SHELL = sys.platform.startswith('win')
WEBRTC_PATH = 'third_party/webrtc'
LIBJINGLE_PATH = 'third_party/libjingle/source/talk'
LIBJINGLE_README = 'third_party/libjingle/README.chromium'
CommitInfo = collections.namedtuple('CommitInfo', ['svn_revision',
'git_commit',
'git_repo_url'])
CLInfo = collections.namedtuple('CLInfo', ['issue', 'url', 'rietveld_server'])
def _ParseSvnRevisionFromGitDescription(description):
for line in reversed(description.splitlines()):
m = GIT_SVN_ID_RE.match(line.strip())
if m:
return m.group(1)
logging.error('Failed to parse svn revision id from:\n%s\n', description)
sys.exit(-1)
def _ParseGitCommitFromDescription(description):
# TODO(kjellander): Consider passing --format=%b to the git log command so we
# don't need to have error-prone parsing like this.
for line in description.splitlines():
if line.startswith('commit '):
return line.split()[1]
logging.error('Failed to parse git commit id from:\n%s\n', description)
sys.exit(-1)
return None
def _ParseDepsFile(filename):
with open(filename, 'rb') as f:
deps_content = f.read()
return _ParseDepsDict(deps_content)
def _ParseDepsDict(deps_content):
local_scope = {}
var = GClientKeywords.VarImpl({}, local_scope)
global_scope = {
'File': GClientKeywords.FileImpl,
'From': GClientKeywords.FromImpl,
'Var': var.Lookup,
'deps_os': {},
}
exec(deps_content, global_scope, local_scope)
return local_scope
def _GenerateCLDescription(webrtc_current, libjingle_current,
webrtc_new, libjingle_new):
delim = ''
webrtc_str = ''
def GetChangeLogURL(git_repo_url, current_hash, new_hash):
return '%s/+log/%s..%s' % (git_repo_url, current_hash[0:7], new_hash[0:7])
if webrtc_current.git_commit != webrtc_new.git_commit:
webrtc_str = 'WebRTC %s:%s' % (webrtc_current.svn_revision,
webrtc_new.svn_revision)
webrtc_changelog_url = GetChangeLogURL(webrtc_current.git_repo_url,
webrtc_current.git_commit,
webrtc_new.git_commit)
libjingle_str = ''
if libjingle_current.git_commit != libjingle_new.git_commit:
if webrtc_str:
delim += ', '
libjingle_str = 'Libjingle %s:%s' % (libjingle_current.svn_revision,
libjingle_new.svn_revision)
libjingle_changelog_url = GetChangeLogURL(libjingle_current.git_repo_url,
libjingle_current.git_commit,
libjingle_new.git_commit)
description = 'Roll ' + webrtc_str + delim + libjingle_str + '\n\n'
if webrtc_str:
description += webrtc_str + '\n'
description += 'Changes: %s\n\n' % webrtc_changelog_url
if libjingle_str:
description += libjingle_str + '\n'
description += 'Changes: %s\n' % libjingle_changelog_url
description += '\nTBR='
return description
def _IsChromiumCheckout(checkout_dir):
"""Checks if the provided directory path is a Chromium checkout."""
# Look for DEPS file and a chromium directory.
return (os.path.isfile(os.path.join(checkout_dir, 'DEPS')) and
os.path.isdir(os.path.join(checkout_dir, 'chrome')))
class AutoRoller(object):
def __init__(self, chromium_src, dry_run, ignore_checks, no_commit):
self._chromium_src = chromium_src
self._dry_run = dry_run
self._ignore_checks = ignore_checks
self._no_commit = no_commit
def _RunCommand(self, command, working_dir=None, ignore_exit_code=False,
extra_env=None):
"""Runs a command and returns the stdout from that command.
If the command fails (exit code != 0), the function will exit the process.
"""
working_dir = working_dir or self._chromium_src
logging.debug('cmd: %s cwd: %s', ' '.join(command), working_dir)
env = os.environ.copy()
if extra_env:
logging.debug('extra env: %s', extra_env)
env.update(extra_env)
p = subprocess.Popen(command, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, shell=USE_SHELL, env=env,
cwd=working_dir, universal_newlines=True)
output = p.stdout.read()
p.wait()
p.stdout.close()
p.stderr.close()
if not ignore_exit_code and p.returncode != 0:
logging.error('Command failed: %s\n%s', str(command), output)
sys.exit(p.returncode)
return output
def _GetCommitInfo(self, path_below_src, git_hash=None, git_repo_url=None):
working_dir = os.path.join(self._chromium_src, path_below_src)
self._RunCommand(['git', 'fetch', 'origin'], working_dir=working_dir)
revision_range = git_hash or 'origin'
ret = self._RunCommand(
['git', '--no-pager', 'log', revision_range, '--pretty=full', '-1'],
working_dir=working_dir)
return CommitInfo(_ParseSvnRevisionFromGitDescription(ret),
_ParseGitCommitFromDescription(ret), git_repo_url)
def _GetDepsCommitInfo(self, deps_dict, path_below_src):
entry = deps_dict['deps']['src/%s' % path_below_src]
at_index = entry.find('@')
git_repo_url = entry[:at_index]
git_hash = entry[at_index + 1:]
return self._GetCommitInfo(path_below_src, git_hash, git_repo_url)
def _GetCLInfo(self):
cl_output = self._RunCommand(['git', 'cl', 'issue'])
m = CL_ISSUE_RE.match(cl_output.strip())
if not m:
logging.error('Cannot find any CL info. Output was:\n%s', cl_output)
sys.exit(-1)
issue_number = int(m.group(1))
url = m.group(2)
# Parse the Rietveld host from the URL.
m = RIETVELD_URL_RE.match(url)
if not m:
logging.error('Cannot parse Rietveld host from URL: %s', url)
sys.exit(-1)
rietveld_server = m.group(1)
return CLInfo(issue_number, url, rietveld_server)
def _GetCurrentBranchName(self):
return self._RunCommand(
['git', 'rev-parse', '--abbrev-ref', 'HEAD']).splitlines()[0]
def _IsTreeClean(self):
lines = self._RunCommand(['git', 'status', '--porcelain']).splitlines()
if len(lines) == 0:
return True
logging.debug('Dirty/unversioned files:\n%s', '\n'.join(lines))
return False
def _UpdateReadmeFile(self, readme_path, new_revision):
readme = open(os.path.join(self._chromium_src, readme_path), 'r+')
txt = readme.read()
m = re.sub(re.compile('.*^Revision\: ([0-9]*).*', re.MULTILINE),
('Revision: %s' % new_revision), txt)
readme.seek(0)
readme.write(m)
readme.truncate()
def PrepareRoll(self):
# TODO(kjellander): use os.path.normcase, os.path.join etc for all paths for
# cross platform compatibility.
if not self._ignore_checks:
if self._GetCurrentBranchName() != 'master':
logging.error('Please checkout the master branch.')
return -1
if not self._IsTreeClean():
logging.error('Please make sure you don\'t have any modified files.')
return -1
logging.debug('Checking for a previous roll branch.')
# TODO(kjellander): switch to the stale branch, close the issue, switch back
# to master,
self._RunCommand(['git', 'branch', '-D', ROLL_BRANCH_NAME],
ignore_exit_code=True)
logging.debug('Pulling latest changes')
if not self._ignore_checks:
self._RunCommand(['git', 'pull'])
self._RunCommand(['git', 'checkout', '-b', ROLL_BRANCH_NAME])
# Modify Chromium's DEPS file.
# Parse current hashes.
deps = _ParseDepsFile(os.path.join(self._chromium_src, 'DEPS'))
webrtc_current = self._GetDepsCommitInfo(deps, WEBRTC_PATH)
libjingle_current = self._GetDepsCommitInfo(deps, LIBJINGLE_PATH)
# Find ToT revisions.
webrtc_latest = self._GetCommitInfo(WEBRTC_PATH)
libjingle_latest = self._GetCommitInfo(LIBJINGLE_PATH)
self._RunCommand(['roll-dep', WEBRTC_PATH, webrtc_latest.git_commit])
self._RunCommand(['roll-dep', LIBJINGLE_PATH, libjingle_latest.git_commit])
if self._IsTreeClean():
logging.debug('Tree is clean - no changes detected.')
self._DeleteRollBranch()
else:
self._UpdateReadmeFile(LIBJINGLE_README, libjingle_latest.svn_revision)
description = _GenerateCLDescription(webrtc_current, libjingle_current,
webrtc_latest, libjingle_latest)
logging.debug('Committing changes locally.')
self._RunCommand(['git', 'add', '--update', '.'])
self._RunCommand(['git', 'commit', '-m', description])
logging.debug('Uploading changes...')
self._RunCommand(['git', 'cl', 'upload', '-m', description],
extra_env={'EDITOR': 'true'})
cl_info = self._GetCLInfo()
logging.debug('Issue: %d URL: %s', cl_info.issue, cl_info.url)
if not self._dry_run and not self._no_commit:
logging.debug('Sending the CL to the CQ...')
self._RunCommand(['git', 'cl', 'set_commit'])
logging.debug('Sent the CL to the CQ. Monitor here:\n%s', cl_info.url)
self._DeleteRollBranch()
# TODO(kjellander): Checkout masters/previous branches again.
return 0
def _DeleteRollBranch(self):
self._RunCommand(['git', 'checkout', 'master'])
self._RunCommand(['git', 'branch', '-D', ROLL_BRANCH_NAME])
logging.debug('Deleted the local roll branch (%s)', ROLL_BRANCH_NAME)
def _GetBranches(self):
"""Returns a tuple of active,branches.
The 'active' is the name of the currently active branch and 'branches' is a
list of all branches.
"""
lines = self._RunCommand(['git', 'branch']).split('\n')
branches = []
active = ''
for l in lines:
if '*' in l:
# The assumption is that the first char will always be the '*'.
active = l[1:].strip()
branches.append(active)
else:
b = l.strip()
if b:
branches.append(b)
return (active, branches)
def Abort(self):
active_branch, branches = self._GetBranches()
if active_branch == ROLL_BRANCH_NAME:
active_branch = 'master'
if ROLL_BRANCH_NAME in branches:
print 'Aborting pending roll.'
self._RunCommand(['git', 'checkout', ROLL_BRANCH_NAME])
# Ignore an error here in case an issue wasn't created for some reason.
self._RunCommand(['git', 'cl', 'set_close'], ignore_exit_code=True)
self._RunCommand(['git', 'checkout', active_branch])
self._RunCommand(['git', 'branch', '-D', ROLL_BRANCH_NAME])
return 0
def main():
if sys.platform in ('win32', 'cygwin'):
logging.error('Only Linux and Mac platforms are supported right now.')
return -1
parser = argparse.ArgumentParser(
description='Find webrtc and libjingle revisions for roll.')
parser.add_argument('--chromium-checkout', type=str,
help=('Path to the Chromium checkout (src/ dir) to perform the roll in '
'(optional if the current working dir is a Chromium checkout).'))
parser.add_argument('--abort',
help=('Aborts a previously prepared roll. '
'Closes any associated issues and deletes the roll branches'),
action='store_true')
parser.add_argument('--no-commit',
help=('Don\'t send the CL to the CQ. This is useful if additional changes '
'are needed to the CL (like for API changes).'),
action='store_true')
parser.add_argument('--dry-run', action='store_true', default=False,
help='Create branches and CLs but doesn\'t send tryjobs or commit.')
parser.add_argument('--ignore-checks', action='store_true', default=False,
help=('Skips checks for being on the master branch, dirty workspaces and '
'the updating of the checkout. Will still delete and create local '
'Git branches.'))
parser.add_argument('-v', '--verbose', action='store_true', default=False,
help='Be extra verbose in printing of log messages.')
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.ERROR)
if args.chromium_checkout:
if not _IsChromiumCheckout(args.chromium_checkout):
logging.error('Cannot find the specified Chromium checkout at: %s',
args.chromium_checkout)
return -2
else:
# Try fallback on current working directory:
cwd = os.getcwd()
if _IsChromiumCheckout(cwd):
args.chromium_checkout = cwd
else:
logging.error(
'--chromium-checkout not specified and the current working directory '
' is not a Chromium checkout. Fix either and try again.')
return -2
autoroller = AutoRoller(args.chromium_checkout, args.dry_run,
args.ignore_checks, args.no_commit)
if args.abort:
return autoroller.Abort()
else:
return autoroller.PrepareRoll()
if __name__ == '__main__':
sys.exit(main())