blob: c80184ce18e8bb2a8c80ef9d445b4df9186511bd [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010016import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010026import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000028import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000037 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
skobes6468b902016-10-24 08:45:10 -070045import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
tandrii9d2c7a32016-06-22 03:42:45 -070067COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070068DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000069POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000071GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000072REFS_THAT_ALIAS_TO_OTHER_REFS = {
73 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
74 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
75}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
thestig@chromium.org44202a22014-03-11 19:22:18 +000077# Valid extensions for files we want to lint.
78DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
79DEFAULT_LINT_IGNORE_REGEX = r"$^"
80
borenet6c0efe62016-10-19 08:13:29 -070081# Buildbucket master name prefix.
82MASTER_PREFIX = 'master.'
83
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000084# Shortcut since it quickly becomes redundant.
85Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000086
maruel@chromium.orgddd59412011-11-30 14:20:38 +000087# Initialized in main()
88settings = None
89
90
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070092 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093 sys.exit(1)
94
95
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000096def GetNoGitPagerEnv():
97 env = os.environ.copy()
98 # 'cat' is a magical git string that disables pagers on all platforms.
99 env['GIT_PAGER'] = 'cat'
100 return env
101
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000102
bsep@chromium.org627d9002016-04-29 00:00:52 +0000103def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000105 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000106 except subprocess2.CalledProcessError as e:
107 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000110 'Command "%s" failed.\n%s' % (
111 ' '.join(args), error_message or e.stdout or ''))
112 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
115def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000117 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000118
119
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000120def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000121 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700122 if suppress_stderr:
123 stderr = subprocess2.VOID
124 else:
125 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 try:
tandrii5d48c322016-08-18 16:19:37 -0700127 (out, _), code = subprocess2.communicate(['git'] + args,
128 env=GetNoGitPagerEnv(),
129 stdout=subprocess2.PIPE,
130 stderr=stderr)
131 return code, out
132 except subprocess2.CalledProcessError as e:
133 logging.debug('Failed running %s', args)
134 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000135
136
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000138 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000139 return RunGitWithCode(args, suppress_stderr=True)[1]
140
141
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000144 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000145 return (version.startswith(prefix) and
146 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000147
148
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000149def BranchExists(branch):
150 """Return True if specified branch exists."""
151 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
152 suppress_stderr=True)
153 return not code
154
155
tandrii2a16b952016-10-19 07:09:44 -0700156def time_sleep(seconds):
157 # Use this so that it can be mocked in tests without interfering with python
158 # system machinery.
159 import time # Local import to discourage others from importing time globally.
160 return time.sleep(seconds)
161
162
maruel@chromium.org90541732011-04-01 17:54:18 +0000163def ask_for_data(prompt):
164 try:
165 return raw_input(prompt)
166 except KeyboardInterrupt:
167 # Hide the exception.
168 sys.exit(1)
169
170
tandrii5d48c322016-08-18 16:19:37 -0700171def _git_branch_config_key(branch, key):
172 """Helper method to return Git config key for a branch."""
173 assert branch, 'branch name is required to set git config for it'
174 return 'branch.%s.%s' % (branch, key)
175
176
177def _git_get_branch_config_value(key, default=None, value_type=str,
178 branch=False):
179 """Returns git config value of given or current branch if any.
180
181 Returns default in all other cases.
182 """
183 assert value_type in (int, str, bool)
184 if branch is False: # Distinguishing default arg value from None.
185 branch = GetCurrentBranch()
186
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000187 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700188 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000189
tandrii5d48c322016-08-18 16:19:37 -0700190 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700191 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700192 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700193 # git config also has --int, but apparently git config suffers from integer
194 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700195 args.append(_git_branch_config_key(branch, key))
196 code, out = RunGitWithCode(args)
197 if code == 0:
198 value = out.strip()
199 if value_type == int:
200 return int(value)
201 if value_type == bool:
202 return bool(value.lower() == 'true')
203 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 return default
205
206
tandrii5d48c322016-08-18 16:19:37 -0700207def _git_set_branch_config_value(key, value, branch=None, **kwargs):
208 """Sets the value or unsets if it's None of a git branch config.
209
210 Valid, though not necessarily existing, branch must be provided,
211 otherwise currently checked out branch is used.
212 """
213 if not branch:
214 branch = GetCurrentBranch()
215 assert branch, 'a branch name OR currently checked out branch is required'
216 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700217 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700218 if value is None:
219 args.append('--unset')
220 elif isinstance(value, bool):
221 args.append('--bool')
222 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700223 else:
tandrii33a46ff2016-08-23 05:53:40 -0700224 # git config also has --int, but apparently git config suffers from integer
225 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700226 value = str(value)
227 args.append(_git_branch_config_key(branch, key))
228 if value is not None:
229 args.append(value)
230 RunGit(args, **kwargs)
231
232
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100233def _get_committer_timestamp(commit):
234 """Returns unix timestamp as integer of a committer in a commit.
235
236 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
237 """
238 # Git also stores timezone offset, but it only affects visual display,
239 # actual point in time is defined by this timestamp only.
240 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
241
242
243def _git_amend_head(message, committer_timestamp):
244 """Amends commit with new message and desired committer_timestamp.
245
246 Sets committer timezone to UTC.
247 """
248 env = os.environ.copy()
249 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
250 return RunGit(['commit', '--amend', '-m', message], env=env)
251
252
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000253def add_git_similarity(parser):
254 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700255 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000256 help='Sets the percentage that a pair of files need to match in order to'
257 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000258 parser.add_option(
259 '--find-copies', action='store_true',
260 help='Allows git to look for copies.')
261 parser.add_option(
262 '--no-find-copies', action='store_false', dest='find_copies',
263 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000264
265 old_parser_args = parser.parse_args
266 def Parse(args):
267 options, args = old_parser_args(args)
268
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000269 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700270 options.similarity = _git_get_branch_config_value(
271 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000272 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000273 print('Note: Saving similarity of %d%% in git config.'
274 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700275 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000276
iannucci@chromium.org79540052012-10-19 23:15:26 +0000277 options.similarity = max(0, min(options.similarity, 100))
278
279 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700280 options.find_copies = _git_get_branch_config_value(
281 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000282 else:
tandrii5d48c322016-08-18 16:19:37 -0700283 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000284
285 print('Using %d%% similarity for rename/copy detection. '
286 'Override with --similarity.' % options.similarity)
287
288 return options, args
289 parser.parse_args = Parse
290
291
machenbach@chromium.org45453142015-09-15 08:45:22 +0000292def _get_properties_from_options(options):
293 properties = dict(x.split('=', 1) for x in options.properties)
294 for key, val in properties.iteritems():
295 try:
296 properties[key] = json.loads(val)
297 except ValueError:
298 pass # If a value couldn't be evaluated, treat it as a string.
299 return properties
300
301
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000302def _prefix_master(master):
303 """Convert user-specified master name to full master name.
304
305 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
306 name, while the developers always use shortened master name
307 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
308 function does the conversion for buildbucket migration.
309 """
borenet6c0efe62016-10-19 08:13:29 -0700310 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000311 return master
borenet6c0efe62016-10-19 08:13:29 -0700312 return '%s%s' % (MASTER_PREFIX, master)
313
314
315def _unprefix_master(bucket):
316 """Convert bucket name to shortened master name.
317
318 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
319 name, while the developers always use shortened master name
320 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
321 function does the conversion for buildbucket migration.
322 """
323 if bucket.startswith(MASTER_PREFIX):
324 return bucket[len(MASTER_PREFIX):]
325 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000326
327
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000328def _buildbucket_retry(operation_name, http, *args, **kwargs):
329 """Retries requests to buildbucket service and returns parsed json content."""
330 try_count = 0
331 while True:
332 response, content = http.request(*args, **kwargs)
333 try:
334 content_json = json.loads(content)
335 except ValueError:
336 content_json = None
337
338 # Buildbucket could return an error even if status==200.
339 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000340 error = content_json.get('error')
341 if error.get('code') == 403:
342 raise BuildbucketResponseException(
343 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000344 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000346 raise BuildbucketResponseException(msg)
347
348 if response.status == 200:
349 if not content_json:
350 raise BuildbucketResponseException(
351 'Buildbucket returns invalid json content: %s.\n'
352 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
353 content)
354 return content_json
355 if response.status < 500 or try_count >= 2:
356 raise httplib2.HttpLib2Error(content)
357
358 # status >= 500 means transient failures.
359 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700360 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 try_count += 1
362 assert False, 'unreachable'
363
364
qyearsley1fdfcb62016-10-24 13:22:03 -0700365def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700366 """Returns a dict mapping bucket names to builders and tests,
367 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700368 """
qyearsleydd49f942016-10-28 11:57:22 -0700369 # If no bots are listed, we try to get a set of builders and tests based
370 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700371 if not options.bot:
372 change = changelist.GetChange(
373 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700374 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700375 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 change=change,
377 changed_files=change.LocalPaths(),
378 repository_root=settings.GetRoot(),
379 default_presubmit=None,
380 project=None,
381 verbose=options.verbose,
382 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700383 if masters is None:
384 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100385 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700386
qyearsley1fdfcb62016-10-24 13:22:03 -0700387 if options.bucket:
388 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700389 if options.master:
390 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700391
qyearsleydd49f942016-10-28 11:57:22 -0700392 # If bots are listed but no master or bucket, then we need to find out
393 # the corresponding master for each bot.
394 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
395 if error_message:
396 option_parser.error(
397 'Tryserver master cannot be found because: %s\n'
398 'Please manually specify the tryserver master, e.g. '
399 '"-m tryserver.chromium.linux".' % error_message)
400 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700401
402
qyearsley123a4682016-10-26 09:12:17 -0700403def _get_bucket_map_for_builders(builders):
404 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700405 map_url = 'https://builders-map.appspot.com/'
406 try:
qyearsley123a4682016-10-26 09:12:17 -0700407 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700408 except urllib2.URLError as e:
409 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
410 (map_url, e))
411 except ValueError as e:
412 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700413 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 return None, 'Failed to build master map.'
415
qyearsley123a4682016-10-26 09:12:17 -0700416 bucket_map = {}
417 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700418 masters = builders_map.get(builder, [])
419 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700421 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700422 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700423 (builder, masters))
424 bucket = _prefix_master(masters[0])
425 bucket_map.setdefault(bucket, {})[builder] = []
426
427 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700428
429
borenet6c0efe62016-10-19 08:13:29 -0700430def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700431 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700432 """Sends a request to Buildbucket to trigger try jobs for a changelist.
433
434 Args:
435 auth_config: AuthConfig for Rietveld.
436 changelist: Changelist that the try jobs are associated with.
437 buckets: A nested dict mapping bucket names to builders to tests.
438 options: Command-line options.
439 """
tandriide281ae2016-10-12 06:02:30 -0700440 assert changelist.GetIssue(), 'CL must be uploaded first'
441 codereview_url = changelist.GetCodereviewServer()
442 assert codereview_url, 'CL must be uploaded first'
443 patchset = patchset or changelist.GetMostRecentPatchset()
444 assert patchset, 'CL must be uploaded first'
445
446 codereview_host = urlparse.urlparse(codereview_url).hostname
447 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000448 http = authenticator.authorize(httplib2.Http())
449 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700450
451 # TODO(tandrii): consider caching Gerrit CL details just like
452 # _RietveldChangelistImpl does, then caching values in these two variables
453 # won't be necessary.
454 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000455
456 buildbucket_put_url = (
457 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000458 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700459 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
460 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
461 hostname=codereview_host,
462 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000463 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700464
465 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
466 shared_parameters_properties['category'] = category
467 if options.clobber:
468 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700469 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700470 if extra_properties:
471 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000472
473 batch_req_body = {'builds': []}
474 print_text = []
475 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700476 for bucket, builders_and_tests in sorted(buckets.iteritems()):
477 print_text.append('Bucket: %s' % bucket)
478 master = None
479 if bucket.startswith(MASTER_PREFIX):
480 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000481 for builder, tests in sorted(builders_and_tests.iteritems()):
482 print_text.append(' %s: %s' % (builder, tests))
483 parameters = {
484 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000485 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700486 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000487 'revision': options.revision,
488 }],
tandrii8c5a3532016-11-04 07:52:02 -0700489 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000490 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000491 if 'presubmit' in builder.lower():
492 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000493 if tests:
494 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700495
496 tags = [
497 'builder:%s' % builder,
498 'buildset:%s' % buildset,
499 'user_agent:git_cl_try',
500 ]
501 if master:
502 parameters['properties']['master'] = master
503 tags.append('master:%s' % master)
504
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000505 batch_req_body['builds'].append(
506 {
507 'bucket': bucket,
508 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000509 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700510 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000511 }
512 )
513
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000514 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700515 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000516 http,
517 buildbucket_put_url,
518 'PUT',
519 body=json.dumps(batch_req_body),
520 headers={'Content-Type': 'application/json'}
521 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000522 print_text.append('To see results here, run: git cl try-results')
523 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700524 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000525
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000526
tandrii221ab252016-10-06 08:12:04 -0700527def fetch_try_jobs(auth_config, changelist, buildbucket_host,
528 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700529 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000530
qyearsley53f48a12016-09-01 10:45:13 -0700531 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532 """
tandrii221ab252016-10-06 08:12:04 -0700533 assert buildbucket_host
534 assert changelist.GetIssue(), 'CL must be uploaded first'
535 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
536 patchset = patchset or changelist.GetMostRecentPatchset()
537 assert patchset, 'CL must be uploaded first'
538
539 codereview_url = changelist.GetCodereviewServer()
540 codereview_host = urlparse.urlparse(codereview_url).hostname
541 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542 if authenticator.has_cached_credentials():
543 http = authenticator.authorize(httplib2.Http())
544 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700545 print('Warning: Some results might be missing because %s' %
546 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700547 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 http = httplib2.Http()
549
550 http.force_exception_to_status_code = True
551
tandrii221ab252016-10-06 08:12:04 -0700552 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
553 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
554 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000555 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700556 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 params = {'tag': 'buildset:%s' % buildset}
558
559 builds = {}
560 while True:
561 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700562 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700564 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 for build in content.get('builds', []):
566 builds[build['id']] = build
567 if 'next_cursor' in content:
568 params['start_cursor'] = content['next_cursor']
569 else:
570 break
571 return builds
572
573
qyearsleyeab3c042016-08-24 09:18:28 -0700574def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 """Prints nicely result of fetch_try_jobs."""
576 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700577 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578 return
579
580 # Make a copy, because we'll be modifying builds dictionary.
581 builds = builds.copy()
582 builder_names_cache = {}
583
584 def get_builder(b):
585 try:
586 return builder_names_cache[b['id']]
587 except KeyError:
588 try:
589 parameters = json.loads(b['parameters_json'])
590 name = parameters['builder_name']
591 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700592 print('WARNING: failed to get builder name for build %s: %s' % (
593 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000594 name = None
595 builder_names_cache[b['id']] = name
596 return name
597
598 def get_bucket(b):
599 bucket = b['bucket']
600 if bucket.startswith('master.'):
601 return bucket[len('master.'):]
602 return bucket
603
604 if options.print_master:
605 name_fmt = '%%-%ds %%-%ds' % (
606 max(len(str(get_bucket(b))) for b in builds.itervalues()),
607 max(len(str(get_builder(b))) for b in builds.itervalues()))
608 def get_name(b):
609 return name_fmt % (get_bucket(b), get_builder(b))
610 else:
611 name_fmt = '%%-%ds' % (
612 max(len(str(get_builder(b))) for b in builds.itervalues()))
613 def get_name(b):
614 return name_fmt % get_builder(b)
615
616 def sort_key(b):
617 return b['status'], b.get('result'), get_name(b), b.get('url')
618
619 def pop(title, f, color=None, **kwargs):
620 """Pop matching builds from `builds` dict and print them."""
621
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000622 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000623 colorize = str
624 else:
625 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
626
627 result = []
628 for b in builds.values():
629 if all(b.get(k) == v for k, v in kwargs.iteritems()):
630 builds.pop(b['id'])
631 result.append(b)
632 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700633 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000634 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700635 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000636
637 total = len(builds)
638 pop(status='COMPLETED', result='SUCCESS',
639 title='Successes:', color=Fore.GREEN,
640 f=lambda b: (get_name(b), b.get('url')))
641 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
642 title='Infra Failures:', color=Fore.MAGENTA,
643 f=lambda b: (get_name(b), b.get('url')))
644 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
645 title='Failures:', color=Fore.RED,
646 f=lambda b: (get_name(b), b.get('url')))
647 pop(status='COMPLETED', result='CANCELED',
648 title='Canceled:', color=Fore.MAGENTA,
649 f=lambda b: (get_name(b),))
650 pop(status='COMPLETED', result='FAILURE',
651 failure_reason='INVALID_BUILD_DEFINITION',
652 title='Wrong master/builder name:', color=Fore.MAGENTA,
653 f=lambda b: (get_name(b),))
654 pop(status='COMPLETED', result='FAILURE',
655 title='Other failures:',
656 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
657 pop(status='COMPLETED',
658 title='Other finished:',
659 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
660 pop(status='STARTED',
661 title='Started:', color=Fore.YELLOW,
662 f=lambda b: (get_name(b), b.get('url')))
663 pop(status='SCHEDULED',
664 title='Scheduled:',
665 f=lambda b: (get_name(b), 'id=%s' % b['id']))
666 # The last section is just in case buildbucket API changes OR there is a bug.
667 pop(title='Other:',
668 f=lambda b: (get_name(b), 'id=%s' % b['id']))
669 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700670 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000671
672
qyearsley53f48a12016-09-01 10:45:13 -0700673def write_try_results_json(output_file, builds):
674 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
675
676 The input |builds| dict is assumed to be generated by Buildbucket.
677 Buildbucket documentation: http://goo.gl/G0s101
678 """
679
680 def convert_build_dict(build):
681 return {
682 'buildbucket_id': build.get('id'),
683 'status': build.get('status'),
684 'result': build.get('result'),
685 'bucket': build.get('bucket'),
686 'builder_name': json.loads(
687 build.get('parameters_json', '{}')).get('builder_name'),
688 'failure_reason': build.get('failure_reason'),
689 'url': build.get('url'),
690 }
691
692 converted = []
693 for _, build in sorted(builds.items()):
694 converted.append(convert_build_dict(build))
695 write_json(output_file, converted)
696
697
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000698def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
699 """Return the corresponding git ref if |base_url| together with |glob_spec|
700 matches the full |url|.
701
702 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
703 """
704 fetch_suburl, as_ref = glob_spec.split(':')
705 if allow_wildcards:
706 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
707 if glob_match:
708 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
709 # "branches/{472,597,648}/src:refs/remotes/svn/*".
710 branch_re = re.escape(base_url)
711 if glob_match.group(1):
712 branch_re += '/' + re.escape(glob_match.group(1))
713 wildcard = glob_match.group(2)
714 if wildcard == '*':
715 branch_re += '([^/]*)'
716 else:
717 # Escape and replace surrounding braces with parentheses and commas
718 # with pipe symbols.
719 wildcard = re.escape(wildcard)
720 wildcard = re.sub('^\\\\{', '(', wildcard)
721 wildcard = re.sub('\\\\,', '|', wildcard)
722 wildcard = re.sub('\\\\}$', ')', wildcard)
723 branch_re += wildcard
724 if glob_match.group(3):
725 branch_re += re.escape(glob_match.group(3))
726 match = re.match(branch_re, url)
727 if match:
728 return re.sub('\*$', match.group(1), as_ref)
729
730 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
731 if fetch_suburl:
732 full_url = base_url + '/' + fetch_suburl
733 else:
734 full_url = base_url
735 if full_url == url:
736 return as_ref
737 return None
738
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000739
iannucci@chromium.org79540052012-10-19 23:15:26 +0000740def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000741 """Prints statistics about the change to the user."""
742 # --no-ext-diff is broken in some versions of Git, so try to work around
743 # this by overriding the environment (but there is still a problem if the
744 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000745 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000746 if 'GIT_EXTERNAL_DIFF' in env:
747 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000748
749 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800750 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000751 else:
752 similarity_options = ['-M%s' % similarity]
753
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000754 try:
755 stdout = sys.stdout.fileno()
756 except AttributeError:
757 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000758 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000759 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000760 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000761 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000762
763
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000764class BuildbucketResponseException(Exception):
765 pass
766
767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768class Settings(object):
769 def __init__(self):
770 self.default_server = None
771 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000772 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773 self.is_git_svn = None
774 self.svn_branch = None
775 self.tree_status_url = None
776 self.viewvc_url = None
777 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000778 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000779 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000780 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000781 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000782 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000783 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000784 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785
786 def LazyUpdateIfNeeded(self):
787 """Updates the settings from a codereview.settings file, if available."""
788 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000789 # The only value that actually changes the behavior is
790 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000791 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000792 error_ok=True
793 ).strip().lower()
794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000796 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 LoadCodereviewSettingsFromFile(cr_settings_file)
798 self.updated = True
799
800 def GetDefaultServerUrl(self, error_ok=False):
801 if not self.default_server:
802 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000803 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805 if error_ok:
806 return self.default_server
807 if not self.default_server:
808 error_message = ('Could not find settings file. You must configure '
809 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000810 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 return self.default_server
813
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000814 @staticmethod
815 def GetRelativeRoot():
816 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000819 if self.root is None:
820 self.root = os.path.abspath(self.GetRelativeRoot())
821 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000823 def GetGitMirror(self, remote='origin'):
824 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000825 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000826 if not os.path.isdir(local_url):
827 return None
828 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
829 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
830 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
831 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
832 if mirror.exists():
833 return mirror
834 return None
835
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 def GetIsGitSvn(self):
837 """Return true if this repo looks like it's using git-svn."""
838 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000839 if self.GetPendingRefPrefix():
840 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
841 self.is_git_svn = False
842 else:
843 # If you have any "svn-remote.*" config keys, we think you're using svn.
844 self.is_git_svn = RunGitWithCode(
845 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 return self.is_git_svn
847
848 def GetSVNBranch(self):
849 if self.svn_branch is None:
850 if not self.GetIsGitSvn():
851 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
852
853 # Try to figure out which remote branch we're based on.
854 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000855 # 1) iterate through our branch history and find the svn URL.
856 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857
858 # regexp matching the git-svn line that contains the URL.
859 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
860
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000861 # We don't want to go through all of history, so read a line from the
862 # pipe at a time.
863 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000864 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000865 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
866 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000867 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000868 for line in proc.stdout:
869 match = git_svn_re.match(line)
870 if match:
871 url = match.group(1)
872 proc.stdout.close() # Cut pipe.
873 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000875 if url:
876 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
877 remotes = RunGit(['config', '--get-regexp',
878 r'^svn-remote\..*\.url']).splitlines()
879 for remote in remotes:
880 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000882 remote = match.group(1)
883 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000884 rewrite_root = RunGit(
885 ['config', 'svn-remote.%s.rewriteRoot' % remote],
886 error_ok=True).strip()
887 if rewrite_root:
888 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000889 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000890 ['config', 'svn-remote.%s.fetch' % remote],
891 error_ok=True).strip()
892 if fetch_spec:
893 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
894 if self.svn_branch:
895 break
896 branch_spec = RunGit(
897 ['config', 'svn-remote.%s.branches' % remote],
898 error_ok=True).strip()
899 if branch_spec:
900 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
901 if self.svn_branch:
902 break
903 tag_spec = RunGit(
904 ['config', 'svn-remote.%s.tags' % remote],
905 error_ok=True).strip()
906 if tag_spec:
907 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
908 if self.svn_branch:
909 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910
911 if not self.svn_branch:
912 DieWithError('Can\'t guess svn branch -- try specifying it on the '
913 'command line')
914
915 return self.svn_branch
916
917 def GetTreeStatusUrl(self, error_ok=False):
918 if not self.tree_status_url:
919 error_message = ('You must configure your tree status URL by running '
920 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000921 self.tree_status_url = self._GetRietveldConfig(
922 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923 return self.tree_status_url
924
925 def GetViewVCUrl(self):
926 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000927 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 return self.viewvc_url
929
rmistry@google.com90752582014-01-14 21:04:50 +0000930 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000931 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000932
rmistry@google.com78948ed2015-07-08 23:09:57 +0000933 def GetIsSkipDependencyUpload(self, branch_name):
934 """Returns true if specified branch should skip dep uploads."""
935 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
936 error_ok=True)
937
rmistry@google.com5626a922015-02-26 14:03:30 +0000938 def GetRunPostUploadHook(self):
939 run_post_upload_hook = self._GetRietveldConfig(
940 'run-post-upload-hook', error_ok=True)
941 return run_post_upload_hook == "True"
942
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000943 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000944 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000945
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000946 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000947 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000948
ukai@chromium.orge8077812012-02-03 03:41:46 +0000949 def GetIsGerrit(self):
950 """Return true if this repo is assosiated with gerrit code review system."""
951 if self.is_gerrit is None:
952 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
953 return self.is_gerrit
954
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000955 def GetSquashGerritUploads(self):
956 """Return true if uploads to Gerrit should be squashed by default."""
957 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700958 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
959 if self.squash_gerrit_uploads is None:
960 # Default is squash now (http://crbug.com/611892#c23).
961 self.squash_gerrit_uploads = not (
962 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
963 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000964 return self.squash_gerrit_uploads
965
tandriia60502f2016-06-20 02:01:53 -0700966 def GetSquashGerritUploadsOverride(self):
967 """Return True or False if codereview.settings should be overridden.
968
969 Returns None if no override has been defined.
970 """
971 # See also http://crbug.com/611892#c23
972 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
973 error_ok=True).strip()
974 if result == 'true':
975 return True
976 if result == 'false':
977 return False
978 return None
979
tandrii@chromium.org28253532016-04-14 13:46:56 +0000980 def GetGerritSkipEnsureAuthenticated(self):
981 """Return True if EnsureAuthenticated should not be done for Gerrit
982 uploads."""
983 if self.gerrit_skip_ensure_authenticated is None:
984 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000985 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000986 error_ok=True).strip() == 'true')
987 return self.gerrit_skip_ensure_authenticated
988
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000989 def GetGitEditor(self):
990 """Return the editor specified in the git config, or None if none is."""
991 if self.git_editor is None:
992 self.git_editor = self._GetConfig('core.editor', error_ok=True)
993 return self.git_editor or None
994
thestig@chromium.org44202a22014-03-11 19:22:18 +0000995 def GetLintRegex(self):
996 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
997 DEFAULT_LINT_REGEX)
998
999 def GetLintIgnoreRegex(self):
1000 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
1001 DEFAULT_LINT_IGNORE_REGEX)
1002
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001003 def GetProject(self):
1004 if not self.project:
1005 self.project = self._GetRietveldConfig('project', error_ok=True)
1006 return self.project
1007
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001008 def GetForceHttpsCommitUrl(self):
1009 if not self.force_https_commit_url:
1010 self.force_https_commit_url = self._GetRietveldConfig(
1011 'force-https-commit-url', error_ok=True)
1012 return self.force_https_commit_url
1013
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001014 def GetPendingRefPrefix(self):
1015 if not self.pending_ref_prefix:
1016 self.pending_ref_prefix = self._GetRietveldConfig(
1017 'pending-ref-prefix', error_ok=True)
1018 return self.pending_ref_prefix
1019
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001020 def _GetRietveldConfig(self, param, **kwargs):
1021 return self._GetConfig('rietveld.' + param, **kwargs)
1022
rmistry@google.com78948ed2015-07-08 23:09:57 +00001023 def _GetBranchConfig(self, branch_name, param, **kwargs):
1024 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 def _GetConfig(self, param, **kwargs):
1027 self.LazyUpdateIfNeeded()
1028 return RunGit(['config', param], **kwargs).strip()
1029
1030
Andrii Shyshkalov5fb47742016-11-24 17:09:40 +01001031def ShouldGenerateGitNumberFooters():
1032 """Decides depending on codereview.settings file in the current checkout HEAD.
1033 """
1034 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1035 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1036 cr_settings_file = FindCodereviewSettingsFile()
1037 if not cr_settings_file:
1038 return False
1039 keyvals = gclient_utils.ParseCodereviewSettingsContent(
1040 cr_settings_file.read())
Andrii Shyshkalovb8c535f2016-11-24 18:01:52 +01001041 return keyvals.get('GENERATE_GIT_NUMBER_FOOTERS', '').lower() == 'true'
Andrii Shyshkalov5fb47742016-11-24 17:09:40 +01001042
1043
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001044class _GitNumbererState(object):
1045 KNOWN_PROJECTS_WHITELIST = [
1046 'chromium/src',
1047 'external/webrtc',
1048 'v8/v8',
1049 ]
1050
1051 @classmethod
1052 def load(cls, remote_url, remote_ref):
1053 """Figures out the state by fetching special refs from remote repo.
1054 """
1055 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
1056 url_parts = urlparse.urlparse(remote_url)
1057 project_name = url_parts.path.lstrip('/').rstrip('git./')
1058 for known in cls.KNOWN_PROJECTS_WHITELIST:
1059 if project_name.endswith(known):
1060 break
1061 else:
1062 # Early exit to avoid extra fetches for repos that aren't using gnumbd.
1063 return cls(cls._get_pending_prefix_fallback(), None)
1064
1065 # This pollutes local ref space, but the amount of objects is neglible.
1066 error, _ = cls._run_git_with_code([
1067 'fetch', remote_url,
1068 '+refs/meta/config:refs/git_cl/meta/config',
1069 '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
1070 if error:
1071 # Some ref doesn't exist or isn't accessible to current user.
1072 # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
1073 # with git-numberer.
1074 cls._warn('failed to fetch gnumbd and project config for %s: %s',
1075 remote_url, error)
1076 return cls(cls._get_pending_prefix_fallback(), None)
1077 return cls(cls._get_pending_prefix(remote_ref),
1078 cls._is_validator_enabled(remote_ref))
1079
1080 @classmethod
1081 def _get_pending_prefix(cls, ref):
1082 error, gnumbd_config_data = cls._run_git_with_code(
1083 ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
1084 if error:
1085 cls._warn('gnumbd config file not found')
1086 return cls._get_pending_prefix_fallback()
1087
1088 try:
1089 config = json.loads(gnumbd_config_data)
1090 if cls.match_refglobs(ref, config['enabled_refglobs']):
1091 return config['pending_ref_prefix']
1092 return None
1093 except KeyboardInterrupt:
1094 raise
1095 except Exception as e:
1096 cls._warn('failed to parse gnumbd config: %s', e)
1097 return cls._get_pending_prefix_fallback()
1098
1099 @staticmethod
1100 def _get_pending_prefix_fallback():
1101 global settings
1102 if not settings:
1103 settings = Settings()
1104 return settings.GetPendingRefPrefix()
1105
1106 @classmethod
1107 def _is_validator_enabled(cls, ref):
1108 error, project_config_data = cls._run_git_with_code(
1109 ['show', 'refs/git_cl/meta/config:project.config'])
1110 if error:
1111 cls._warn('project.config file not found')
1112 return False
1113 # Gerrit's project.config is really a git config file.
1114 # So, parse it as such.
1115 with tempfile.NamedTemporaryFile(prefix='git_cl_proj_config') as f:
1116 f.write(project_config_data)
1117 # Make sure OS sees this, but don't close the file just yet,
1118 # as NamedTemporaryFile deletes it on closing.
1119 f.flush()
1120
1121 def get_opts(x):
1122 code, out = cls._run_git_with_code(
1123 ['config', '-f', f.name, '--get-all',
1124 'plugin.git-numberer.validate-%s-refglob' % x])
1125 if code == 0:
1126 return out.strip().splitlines()
1127 return []
1128 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
1129
1130 if cls.match_refglobs(ref, disabled):
1131 return False
1132 return cls.match_refglobs(ref, enabled)
1133
1134 @staticmethod
1135 def match_refglobs(ref, refglobs):
1136 for refglob in refglobs:
1137 if ref == refglob or fnmatch.fnmatch(ref, refglob):
1138 return True
1139 return False
1140
1141 @staticmethod
1142 def _run_git_with_code(*args, **kwargs):
1143 # The only reason for this wrapper is easy porting of this code to CQ
1144 # codebase, which forked git_cl.py and checkouts.py long time ago.
1145 return RunGitWithCode(*args, **kwargs)
1146
1147 @staticmethod
1148 def _warn(msg, *args):
1149 if args:
1150 msg = msg % args
1151 print('WARNING: %s' % msg)
1152
1153 def __init__(self, pending_prefix, validator_enabled):
1154 # TODO(tandrii): remove pending_prefix after gnumbd is no more.
1155 self._pending_prefix = pending_prefix or None
1156 self._validator_enabled = validator_enabled or False
1157
1158 @property
1159 def pending_prefix(self):
1160 return self._pending_prefix
1161
1162 @property
1163 def should_git_number(self):
1164 return self._validator_enabled and self._pending_prefix is None
1165
1166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167def ShortBranchName(branch):
1168 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 return branch.replace('refs/heads/', '', 1)
1170
1171
1172def GetCurrentBranchRef():
1173 """Returns branch ref (e.g., refs/heads/master) or None."""
1174 return RunGit(['symbolic-ref', 'HEAD'],
1175 stderr=subprocess2.VOID, error_ok=True).strip() or None
1176
1177
1178def GetCurrentBranch():
1179 """Returns current branch or None.
1180
1181 For refs/heads/* branches, returns just last part. For others, full ref.
1182 """
1183 branchref = GetCurrentBranchRef()
1184 if branchref:
1185 return ShortBranchName(branchref)
1186 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
1188
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001189class _CQState(object):
1190 """Enum for states of CL with respect to Commit Queue."""
1191 NONE = 'none'
1192 DRY_RUN = 'dry_run'
1193 COMMIT = 'commit'
1194
1195 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1196
1197
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001198class _ParsedIssueNumberArgument(object):
1199 def __init__(self, issue=None, patchset=None, hostname=None):
1200 self.issue = issue
1201 self.patchset = patchset
1202 self.hostname = hostname
1203
1204 @property
1205 def valid(self):
1206 return self.issue is not None
1207
1208
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001209def ParseIssueNumberArgument(arg):
1210 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1211 fail_result = _ParsedIssueNumberArgument()
1212
1213 if arg.isdigit():
1214 return _ParsedIssueNumberArgument(issue=int(arg))
1215 if not arg.startswith('http'):
1216 return fail_result
1217 url = gclient_utils.UpgradeToHttps(arg)
1218 try:
1219 parsed_url = urlparse.urlparse(url)
1220 except ValueError:
1221 return fail_result
1222 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1223 tmp = cls.ParseIssueURL(parsed_url)
1224 if tmp is not None:
1225 return tmp
1226 return fail_result
1227
1228
Aaron Gablea45ee112016-11-22 15:14:38 -08001229class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001230 def __init__(self, issue, url):
1231 self.issue = issue
1232 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001233 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001234
1235 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001236 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001237 self.issue, self.url)
1238
1239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 """Changelist works with one changelist in local branch.
1242
1243 Supports two codereview backends: Rietveld or Gerrit, selected at object
1244 creation.
1245
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001246 Notes:
1247 * Not safe for concurrent multi-{thread,process} use.
1248 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001249 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001250 """
1251
1252 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1253 """Create a new ChangeList instance.
1254
1255 If issue is given, the codereview must be given too.
1256
1257 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1258 Otherwise, it's decided based on current configuration of the local branch,
1259 with default being 'rietveld' for backwards compatibility.
1260 See _load_codereview_impl for more details.
1261
1262 **kwargs will be passed directly to codereview implementation.
1263 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001265 global settings
1266 if not settings:
1267 # Happens when git_cl.py is used as a utility library.
1268 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001269
1270 if issue:
1271 assert codereview, 'codereview must be known, if issue is known'
1272
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 self.branchref = branchref
1274 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001275 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 self.branch = ShortBranchName(self.branchref)
1277 else:
1278 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001280 self.lookedup_issue = False
1281 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 self.has_description = False
1283 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001284 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001286 self.cc = None
1287 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001288 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001289
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001290 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001291 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001292 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001293 assert self._codereview_impl
1294 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001295
1296 def _load_codereview_impl(self, codereview=None, **kwargs):
1297 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001298 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1299 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1300 self._codereview = codereview
1301 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001302 return
1303
1304 # Automatic selection based on issue number set for a current branch.
1305 # Rietveld takes precedence over Gerrit.
1306 assert not self.issue
1307 # Whether we find issue or not, we are doing the lookup.
1308 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001309 if self.GetBranch():
1310 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1311 issue = _git_get_branch_config_value(
1312 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1313 if issue:
1314 self._codereview = codereview
1315 self._codereview_impl = cls(self, **kwargs)
1316 self.issue = int(issue)
1317 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001318
1319 # No issue is set for this branch, so decide based on repo-wide settings.
1320 return self._load_codereview_impl(
1321 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1322 **kwargs)
1323
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001324 def IsGerrit(self):
1325 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001326
1327 def GetCCList(self):
1328 """Return the users cc'd on this CL.
1329
agable92bec4f2016-08-24 09:27:27 -07001330 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001331 """
1332 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001333 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001334 more_cc = ','.join(self.watchers)
1335 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1336 return self.cc
1337
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001338 def GetCCListWithoutDefault(self):
1339 """Return the users cc'd on this CL excluding default ones."""
1340 if self.cc is None:
1341 self.cc = ','.join(self.watchers)
1342 return self.cc
1343
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001344 def SetWatchers(self, watchers):
1345 """Set the list of email addresses that should be cc'd based on the changed
1346 files in this CL.
1347 """
1348 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
1350 def GetBranch(self):
1351 """Returns the short branch name, e.g. 'master'."""
1352 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001353 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001354 if not branchref:
1355 return None
1356 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 self.branch = ShortBranchName(self.branchref)
1358 return self.branch
1359
1360 def GetBranchRef(self):
1361 """Returns the full branch name, e.g. 'refs/heads/master'."""
1362 self.GetBranch() # Poke the lazy loader.
1363 return self.branchref
1364
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001365 def ClearBranch(self):
1366 """Clears cached branch data of this object."""
1367 self.branch = self.branchref = None
1368
tandrii5d48c322016-08-18 16:19:37 -07001369 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1370 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1371 kwargs['branch'] = self.GetBranch()
1372 return _git_get_branch_config_value(key, default, **kwargs)
1373
1374 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1375 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1376 assert self.GetBranch(), (
1377 'this CL must have an associated branch to %sset %s%s' %
1378 ('un' if value is None else '',
1379 key,
1380 '' if value is None else ' to %r' % value))
1381 kwargs['branch'] = self.GetBranch()
1382 return _git_set_branch_config_value(key, value, **kwargs)
1383
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 @staticmethod
1385 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001386 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 e.g. 'origin', 'refs/heads/master'
1388 """
1389 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001390 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1391
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001393 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001395 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1396 error_ok=True).strip()
1397 if upstream_branch:
1398 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001400 # Fall back on trying a git-svn upstream branch.
1401 if settings.GetIsGitSvn():
1402 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001404 # Else, try to guess the origin remote.
1405 remote_branches = RunGit(['branch', '-r']).split()
1406 if 'origin/master' in remote_branches:
1407 # Fall back on origin/master if it exits.
1408 remote = 'origin'
1409 upstream_branch = 'refs/heads/master'
1410 elif 'origin/trunk' in remote_branches:
1411 # Fall back on origin/trunk if it exists. Generally a shared
1412 # git-svn clone
1413 remote = 'origin'
1414 upstream_branch = 'refs/heads/trunk'
1415 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001416 DieWithError(
1417 'Unable to determine default branch to diff against.\n'
1418 'Either pass complete "git diff"-style arguments, like\n'
1419 ' git cl upload origin/master\n'
1420 'or verify this branch is set up to track another \n'
1421 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
1423 return remote, upstream_branch
1424
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001425 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001426 upstream_branch = self.GetUpstreamBranch()
1427 if not BranchExists(upstream_branch):
1428 DieWithError('The upstream for the current branch (%s) does not exist '
1429 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001430 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001431 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 def GetUpstreamBranch(self):
1434 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001435 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001437 upstream_branch = upstream_branch.replace('refs/heads/',
1438 'refs/remotes/%s/' % remote)
1439 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1440 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 self.upstream_branch = upstream_branch
1442 return self.upstream_branch
1443
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001444 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001445 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001446 remote, branch = None, self.GetBranch()
1447 seen_branches = set()
1448 while branch not in seen_branches:
1449 seen_branches.add(branch)
1450 remote, branch = self.FetchUpstreamTuple(branch)
1451 branch = ShortBranchName(branch)
1452 if remote != '.' or branch.startswith('refs/remotes'):
1453 break
1454 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001455 remotes = RunGit(['remote'], error_ok=True).split()
1456 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001457 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001458 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001459 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001460 logging.warning('Could not determine which remote this change is '
1461 'associated with, so defaulting to "%s". This may '
1462 'not be what you want. You may prevent this message '
1463 'by running "git svn info" as documented here: %s',
1464 self._remote,
1465 GIT_INSTRUCTIONS_URL)
1466 else:
1467 logging.warn('Could not determine which remote this change is '
1468 'associated with. You may prevent this message by '
1469 'running "git svn info" as documented here: %s',
1470 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001471 branch = 'HEAD'
1472 if branch.startswith('refs/remotes'):
1473 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001474 elif branch.startswith('refs/branch-heads/'):
1475 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001476 else:
1477 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001478 return self._remote
1479
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001480 def GitSanityChecks(self, upstream_git_obj):
1481 """Checks git repo status and ensures diff is from local commits."""
1482
sbc@chromium.org79706062015-01-14 21:18:12 +00001483 if upstream_git_obj is None:
1484 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001485 print('ERROR: unable to determine current branch (detached HEAD?)',
1486 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001487 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001488 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001489 return False
1490
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001491 # Verify the commit we're diffing against is in our current branch.
1492 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1493 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1494 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001495 print('ERROR: %s is not in the current branch. You may need to rebase '
1496 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001497 return False
1498
1499 # List the commits inside the diff, and verify they are all local.
1500 commits_in_diff = RunGit(
1501 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1502 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1503 remote_branch = remote_branch.strip()
1504 if code != 0:
1505 _, remote_branch = self.GetRemoteBranch()
1506
1507 commits_in_remote = RunGit(
1508 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1509
1510 common_commits = set(commits_in_diff) & set(commits_in_remote)
1511 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001512 print('ERROR: Your diff contains %d commits already in %s.\n'
1513 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1514 'the diff. If you are using a custom git flow, you can override'
1515 ' the reference used for this check with "git config '
1516 'gitcl.remotebranch <git-ref>".' % (
1517 len(common_commits), remote_branch, upstream_git_obj),
1518 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001519 return False
1520 return True
1521
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001522 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001523 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001524
1525 Returns None if it is not set.
1526 """
tandrii5d48c322016-08-18 16:19:37 -07001527 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001528
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001529 def GetGitSvnRemoteUrl(self):
1530 """Return the configured git-svn remote URL parsed from git svn info.
1531
1532 Returns None if it is not set.
1533 """
1534 # URL is dependent on the current directory.
1535 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1536 if data:
1537 keys = dict(line.split(': ', 1) for line in data.splitlines()
1538 if ': ' in line)
1539 return keys.get('URL', None)
1540 return None
1541
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542 def GetRemoteUrl(self):
1543 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1544
1545 Returns None if there is no remote.
1546 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001547 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001548 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1549
1550 # If URL is pointing to a local directory, it is probably a git cache.
1551 if os.path.isdir(url):
1552 url = RunGit(['config', 'remote.%s.url' % remote],
1553 error_ok=True,
1554 cwd=url).strip()
1555 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001557 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001558 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001559 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001560 self.issue = self._GitGetBranchConfigValue(
1561 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001562 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 return self.issue
1564
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565 def GetIssueURL(self):
1566 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567 issue = self.GetIssue()
1568 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001569 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001570 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571
1572 def GetDescription(self, pretty=False):
1573 if not self.has_description:
1574 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576 self.has_description = True
1577 if pretty:
1578 wrapper = textwrap.TextWrapper()
1579 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1580 return wrapper.fill(self.description)
1581 return self.description
1582
1583 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001584 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001585 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001586 self.patchset = self._GitGetBranchConfigValue(
1587 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001588 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589 return self.patchset
1590
1591 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001592 """Set this branch's patchset. If patchset=0, clears the patchset."""
1593 assert self.GetBranch()
1594 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001595 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001596 else:
1597 self.patchset = int(patchset)
1598 self._GitSetBranchConfigValue(
1599 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001601 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001602 """Set this branch's issue. If issue isn't given, clears the issue."""
1603 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001605 issue = int(issue)
1606 self._GitSetBranchConfigValue(
1607 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001608 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001609 codereview_server = self._codereview_impl.GetCodereviewServer()
1610 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001611 self._GitSetBranchConfigValue(
1612 self._codereview_impl.CodereviewServerConfigKey(),
1613 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001614 else:
tandrii5d48c322016-08-18 16:19:37 -07001615 # Reset all of these just to be clean.
1616 reset_suffixes = [
1617 'last-upload-hash',
1618 self._codereview_impl.IssueConfigKey(),
1619 self._codereview_impl.PatchsetConfigKey(),
1620 self._codereview_impl.CodereviewServerConfigKey(),
1621 ] + self._PostUnsetIssueProperties()
1622 for prop in reset_suffixes:
1623 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001624 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001625 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626
dnjba1b0f32016-09-02 12:37:42 -07001627 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001628 if not self.GitSanityChecks(upstream_branch):
1629 DieWithError('\nGit sanity check failure')
1630
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001631 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001632 if not root:
1633 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001634 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001635
1636 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001637 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001638 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001639 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001640 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001641 except subprocess2.CalledProcessError:
1642 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001643 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001644 'This branch probably doesn\'t exist anymore. To reset the\n'
1645 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001646 ' git branch --set-upstream-to origin/master %s\n'
1647 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001648 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001649
maruel@chromium.org52424302012-08-29 15:14:30 +00001650 issue = self.GetIssue()
1651 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001652 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001653 description = self.GetDescription()
1654 else:
1655 # If the change was never uploaded, use the log messages of all commits
1656 # up to the branch point, as git cl upload will prefill the description
1657 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001658 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1659 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001660
1661 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001662 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001663 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001664 name,
1665 description,
1666 absroot,
1667 files,
1668 issue,
1669 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001670 author,
1671 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001672
dsansomee2d6fd92016-09-08 00:10:47 -07001673 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001674 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001675 return self._codereview_impl.UpdateDescriptionRemote(
1676 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001677
1678 def RunHook(self, committing, may_prompt, verbose, change):
1679 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1680 try:
1681 return presubmit_support.DoPresubmitChecks(change, committing,
1682 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1683 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001684 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1685 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001686 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001687 DieWithError(
1688 ('%s\nMaybe your depot_tools is out of date?\n'
1689 'If all fails, contact maruel@') % e)
1690
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001691 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1692 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001693 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1694 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001695 else:
1696 # Assume url.
1697 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1698 urlparse.urlparse(issue_arg))
1699 if not parsed_issue_arg or not parsed_issue_arg.valid:
1700 DieWithError('Failed to parse issue argument "%s". '
1701 'Must be an issue number or a valid URL.' % issue_arg)
1702 return self._codereview_impl.CMDPatchWithParsedIssue(
1703 parsed_issue_arg, reject, nocommit, directory)
1704
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001705 def CMDUpload(self, options, git_diff_args, orig_args):
1706 """Uploads a change to codereview."""
1707 if git_diff_args:
1708 # TODO(ukai): is it ok for gerrit case?
1709 base_branch = git_diff_args[0]
1710 else:
1711 if self.GetBranch() is None:
1712 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1713
1714 # Default to diffing against common ancestor of upstream branch
1715 base_branch = self.GetCommonAncestorWithUpstream()
1716 git_diff_args = [base_branch, 'HEAD']
1717
1718 # Make sure authenticated to codereview before running potentially expensive
1719 # hooks. It is a fast, best efforts check. Codereview still can reject the
1720 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001721 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001722
1723 # Apply watchlists on upload.
1724 change = self.GetChange(base_branch, None)
1725 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1726 files = [f.LocalPath() for f in change.AffectedFiles()]
1727 if not options.bypass_watchlists:
1728 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1729
1730 if not options.bypass_hooks:
1731 if options.reviewers or options.tbr_owners:
1732 # Set the reviewer list now so that presubmit checks can access it.
1733 change_description = ChangeDescription(change.FullDescriptionText())
1734 change_description.update_reviewers(options.reviewers,
1735 options.tbr_owners,
1736 change)
1737 change.SetDescriptionText(change_description.description)
1738 hook_results = self.RunHook(committing=False,
1739 may_prompt=not options.force,
1740 verbose=options.verbose,
1741 change=change)
1742 if not hook_results.should_continue():
1743 return 1
1744 if not options.reviewers and hook_results.reviewers:
1745 options.reviewers = hook_results.reviewers.split(',')
1746
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001747 # TODO(tandrii): Checking local patchset against remote patchset is only
1748 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1749 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001750 latest_patchset = self.GetMostRecentPatchset()
1751 local_patchset = self.GetPatchset()
1752 if (latest_patchset and local_patchset and
1753 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001754 print('The last upload made from this repository was patchset #%d but '
1755 'the most recent patchset on the server is #%d.'
1756 % (local_patchset, latest_patchset))
1757 print('Uploading will still work, but if you\'ve uploaded to this '
1758 'issue from another machine or branch the patch you\'re '
1759 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001760 ask_for_data('About to upload; enter to confirm.')
1761
1762 print_stats(options.similarity, options.find_copies, git_diff_args)
1763 ret = self.CMDUploadChange(options, git_diff_args, change)
1764 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001765 if options.use_commit_queue:
1766 self.SetCQState(_CQState.COMMIT)
1767 elif options.cq_dry_run:
1768 self.SetCQState(_CQState.DRY_RUN)
1769
tandrii5d48c322016-08-18 16:19:37 -07001770 _git_set_branch_config_value('last-upload-hash',
1771 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001772 # Run post upload hooks, if specified.
1773 if settings.GetRunPostUploadHook():
1774 presubmit_support.DoPostUploadExecuter(
1775 change,
1776 self,
1777 settings.GetRoot(),
1778 options.verbose,
1779 sys.stdout)
1780
1781 # Upload all dependencies if specified.
1782 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001783 print()
1784 print('--dependencies has been specified.')
1785 print('All dependent local branches will be re-uploaded.')
1786 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001787 # Remove the dependencies flag from args so that we do not end up in a
1788 # loop.
1789 orig_args.remove('--dependencies')
1790 ret = upload_branch_deps(self, orig_args)
1791 return ret
1792
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001793 def SetCQState(self, new_state):
1794 """Update the CQ state for latest patchset.
1795
1796 Issue must have been already uploaded and known.
1797 """
1798 assert new_state in _CQState.ALL_STATES
1799 assert self.GetIssue()
1800 return self._codereview_impl.SetCQState(new_state)
1801
qyearsley1fdfcb62016-10-24 13:22:03 -07001802 def TriggerDryRun(self):
1803 """Triggers a dry run and prints a warning on failure."""
1804 # TODO(qyearsley): Either re-use this method in CMDset_commit
1805 # and CMDupload, or change CMDtry to trigger dry runs with
1806 # just SetCQState, and catch keyboard interrupt and other
1807 # errors in that method.
1808 try:
1809 self.SetCQState(_CQState.DRY_RUN)
1810 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1811 return 0
1812 except KeyboardInterrupt:
1813 raise
1814 except:
1815 print('WARNING: failed to trigger CQ Dry Run.\n'
1816 'Either:\n'
1817 ' * your project has no CQ\n'
1818 ' * you don\'t have permission to trigger Dry Run\n'
1819 ' * bug in this code (see stack trace below).\n'
1820 'Consider specifying which bots to trigger manually '
1821 'or asking your project owners for permissions '
1822 'or contacting Chrome Infrastructure team at '
1823 'https://www.chromium.org/infra\n\n')
1824 # Still raise exception so that stack trace is printed.
1825 raise
1826
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001827 # Forward methods to codereview specific implementation.
1828
1829 def CloseIssue(self):
1830 return self._codereview_impl.CloseIssue()
1831
1832 def GetStatus(self):
1833 return self._codereview_impl.GetStatus()
1834
1835 def GetCodereviewServer(self):
1836 return self._codereview_impl.GetCodereviewServer()
1837
tandriide281ae2016-10-12 06:02:30 -07001838 def GetIssueOwner(self):
1839 """Get owner from codereview, which may differ from this checkout."""
1840 return self._codereview_impl.GetIssueOwner()
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 def GetApprovingReviewers(self):
1843 return self._codereview_impl.GetApprovingReviewers()
1844
1845 def GetMostRecentPatchset(self):
1846 return self._codereview_impl.GetMostRecentPatchset()
1847
tandriide281ae2016-10-12 06:02:30 -07001848 def CannotTriggerTryJobReason(self):
1849 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1850 return self._codereview_impl.CannotTriggerTryJobReason()
1851
tandrii8c5a3532016-11-04 07:52:02 -07001852 def GetTryjobProperties(self, patchset=None):
1853 """Returns dictionary of properties to launch tryjob."""
1854 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1855
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001856 def __getattr__(self, attr):
1857 # This is because lots of untested code accesses Rietveld-specific stuff
1858 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001859 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001860 # Note that child method defines __getattr__ as well, and forwards it here,
1861 # because _RietveldChangelistImpl is not cleaned up yet, and given
1862 # deprecation of Rietveld, it should probably be just removed.
1863 # Until that time, avoid infinite recursion by bypassing __getattr__
1864 # of implementation class.
1865 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001866
1867
1868class _ChangelistCodereviewBase(object):
1869 """Abstract base class encapsulating codereview specifics of a changelist."""
1870 def __init__(self, changelist):
1871 self._changelist = changelist # instance of Changelist
1872
1873 def __getattr__(self, attr):
1874 # Forward methods to changelist.
1875 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1876 # _RietveldChangelistImpl to avoid this hack?
1877 return getattr(self._changelist, attr)
1878
1879 def GetStatus(self):
1880 """Apply a rough heuristic to give a simple summary of an issue's review
1881 or CQ status, assuming adherence to a common workflow.
1882
1883 Returns None if no issue for this branch, or specific string keywords.
1884 """
1885 raise NotImplementedError()
1886
1887 def GetCodereviewServer(self):
1888 """Returns server URL without end slash, like "https://codereview.com"."""
1889 raise NotImplementedError()
1890
1891 def FetchDescription(self):
1892 """Fetches and returns description from the codereview server."""
1893 raise NotImplementedError()
1894
tandrii5d48c322016-08-18 16:19:37 -07001895 @classmethod
1896 def IssueConfigKey(cls):
1897 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 raise NotImplementedError()
1899
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001900 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001901 def PatchsetConfigKey(cls):
1902 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001903 raise NotImplementedError()
1904
tandrii5d48c322016-08-18 16:19:37 -07001905 @classmethod
1906 def CodereviewServerConfigKey(cls):
1907 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908 raise NotImplementedError()
1909
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001910 def _PostUnsetIssueProperties(self):
1911 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001912 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001913
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914 def GetRieveldObjForPresubmit(self):
1915 # This is an unfortunate Rietveld-embeddedness in presubmit.
1916 # For non-Rietveld codereviews, this probably should return a dummy object.
1917 raise NotImplementedError()
1918
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001919 def GetGerritObjForPresubmit(self):
1920 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1921 return None
1922
dsansomee2d6fd92016-09-08 00:10:47 -07001923 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 """Update the description on codereview site."""
1925 raise NotImplementedError()
1926
1927 def CloseIssue(self):
1928 """Closes the issue."""
1929 raise NotImplementedError()
1930
1931 def GetApprovingReviewers(self):
1932 """Returns a list of reviewers approving the change.
1933
1934 Note: not necessarily committers.
1935 """
1936 raise NotImplementedError()
1937
1938 def GetMostRecentPatchset(self):
1939 """Returns the most recent patchset number from the codereview site."""
1940 raise NotImplementedError()
1941
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001942 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1943 directory):
1944 """Fetches and applies the issue.
1945
1946 Arguments:
1947 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1948 reject: if True, reject the failed patch instead of switching to 3-way
1949 merge. Rietveld only.
1950 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1951 only.
1952 directory: switch to directory before applying the patch. Rietveld only.
1953 """
1954 raise NotImplementedError()
1955
1956 @staticmethod
1957 def ParseIssueURL(parsed_url):
1958 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1959 failed."""
1960 raise NotImplementedError()
1961
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001962 def EnsureAuthenticated(self, force):
1963 """Best effort check that user is authenticated with codereview server.
1964
1965 Arguments:
1966 force: whether to skip confirmation questions.
1967 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001968 raise NotImplementedError()
1969
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001970 def CMDUploadChange(self, options, args, change):
1971 """Uploads a change to codereview."""
1972 raise NotImplementedError()
1973
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001974 def SetCQState(self, new_state):
1975 """Update the CQ state for latest patchset.
1976
1977 Issue must have been already uploaded and known.
1978 """
1979 raise NotImplementedError()
1980
tandriie113dfd2016-10-11 10:20:12 -07001981 def CannotTriggerTryJobReason(self):
1982 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1983 raise NotImplementedError()
1984
tandriide281ae2016-10-12 06:02:30 -07001985 def GetIssueOwner(self):
1986 raise NotImplementedError()
1987
tandrii8c5a3532016-11-04 07:52:02 -07001988 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001989 raise NotImplementedError()
1990
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001991
1992class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1993 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1994 super(_RietveldChangelistImpl, self).__init__(changelist)
1995 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001996 if not rietveld_server:
1997 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001998
1999 self._rietveld_server = rietveld_server
2000 self._auth_config = auth_config
2001 self._props = None
2002 self._rpc_server = None
2003
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002004 def GetCodereviewServer(self):
2005 if not self._rietveld_server:
2006 # If we're on a branch then get the server potentially associated
2007 # with that branch.
2008 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002009 self._rietveld_server = gclient_utils.UpgradeToHttps(
2010 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002011 if not self._rietveld_server:
2012 self._rietveld_server = settings.GetDefaultServerUrl()
2013 return self._rietveld_server
2014
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002015 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002016 """Best effort check that user is authenticated with Rietveld server."""
2017 if self._auth_config.use_oauth2:
2018 authenticator = auth.get_authenticator_for_host(
2019 self.GetCodereviewServer(), self._auth_config)
2020 if not authenticator.has_cached_credentials():
2021 raise auth.LoginRequiredError(self.GetCodereviewServer())
2022
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002023 def FetchDescription(self):
2024 issue = self.GetIssue()
2025 assert issue
2026 try:
2027 return self.RpcServer().get_description(issue).strip()
2028 except urllib2.HTTPError as e:
2029 if e.code == 404:
2030 DieWithError(
2031 ('\nWhile fetching the description for issue %d, received a '
2032 '404 (not found)\n'
2033 'error. It is likely that you deleted this '
2034 'issue on the server. If this is the\n'
2035 'case, please run\n\n'
2036 ' git cl issue 0\n\n'
2037 'to clear the association with the deleted issue. Then run '
2038 'this command again.') % issue)
2039 else:
2040 DieWithError(
2041 '\nFailed to fetch issue description. HTTP error %d' % e.code)
2042 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07002043 print('Warning: Failed to retrieve CL description due to network '
2044 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002045 return ''
2046
2047 def GetMostRecentPatchset(self):
2048 return self.GetIssueProperties()['patchsets'][-1]
2049
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002050 def GetIssueProperties(self):
2051 if self._props is None:
2052 issue = self.GetIssue()
2053 if not issue:
2054 self._props = {}
2055 else:
2056 self._props = self.RpcServer().get_issue_properties(issue, True)
2057 return self._props
2058
tandriie113dfd2016-10-11 10:20:12 -07002059 def CannotTriggerTryJobReason(self):
2060 props = self.GetIssueProperties()
2061 if not props:
2062 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
2063 if props.get('closed'):
2064 return 'CL %s is closed' % self.GetIssue()
2065 if props.get('private'):
2066 return 'CL %s is private' % self.GetIssue()
2067 return None
2068
tandrii8c5a3532016-11-04 07:52:02 -07002069 def GetTryjobProperties(self, patchset=None):
2070 """Returns dictionary of properties to launch tryjob."""
2071 project = (self.GetIssueProperties() or {}).get('project')
2072 return {
2073 'issue': self.GetIssue(),
2074 'patch_project': project,
2075 'patch_storage': 'rietveld',
2076 'patchset': patchset or self.GetPatchset(),
2077 'rietveld': self.GetCodereviewServer(),
2078 }
2079
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002080 def GetApprovingReviewers(self):
2081 return get_approving_reviewers(self.GetIssueProperties())
2082
tandriide281ae2016-10-12 06:02:30 -07002083 def GetIssueOwner(self):
2084 return (self.GetIssueProperties() or {}).get('owner_email')
2085
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002086 def AddComment(self, message):
2087 return self.RpcServer().add_comment(self.GetIssue(), message)
2088
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002089 def GetStatus(self):
2090 """Apply a rough heuristic to give a simple summary of an issue's review
2091 or CQ status, assuming adherence to a common workflow.
2092
2093 Returns None if no issue for this branch, or one of the following keywords:
2094 * 'error' - error from review tool (including deleted issues)
2095 * 'unsent' - not sent for review
2096 * 'waiting' - waiting for review
2097 * 'reply' - waiting for owner to reply to review
2098 * 'lgtm' - LGTM from at least one approved reviewer
2099 * 'commit' - in the commit queue
2100 * 'closed' - closed
2101 """
2102 if not self.GetIssue():
2103 return None
2104
2105 try:
2106 props = self.GetIssueProperties()
2107 except urllib2.HTTPError:
2108 return 'error'
2109
2110 if props.get('closed'):
2111 # Issue is closed.
2112 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002113 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002114 # Issue is in the commit queue.
2115 return 'commit'
2116
2117 try:
2118 reviewers = self.GetApprovingReviewers()
2119 except urllib2.HTTPError:
2120 return 'error'
2121
2122 if reviewers:
2123 # Was LGTM'ed.
2124 return 'lgtm'
2125
2126 messages = props.get('messages') or []
2127
tandrii9d2c7a32016-06-22 03:42:45 -07002128 # Skip CQ messages that don't require owner's action.
2129 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2130 if 'Dry run:' in messages[-1]['text']:
2131 messages.pop()
2132 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2133 # This message always follows prior messages from CQ,
2134 # so skip this too.
2135 messages.pop()
2136 else:
2137 # This is probably a CQ messages warranting user attention.
2138 break
2139
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002140 if not messages:
2141 # No message was sent.
2142 return 'unsent'
2143 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002144 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002145 return 'reply'
2146 return 'waiting'
2147
dsansomee2d6fd92016-09-08 00:10:47 -07002148 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002149 return self.RpcServer().update_description(
2150 self.GetIssue(), self.description)
2151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002153 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002155 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002156 return self.SetFlags({flag: value})
2157
2158 def SetFlags(self, flags):
2159 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002160 """
phajdan.jr68598232016-08-10 03:28:28 -07002161 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002162 try:
tandrii4b233bd2016-07-06 03:50:29 -07002163 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002164 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002165 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002166 if e.code == 404:
2167 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2168 if e.code == 403:
2169 DieWithError(
2170 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002171 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002172 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002173
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002174 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002175 """Returns an upload.RpcServer() to access this review's rietveld instance.
2176 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002177 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002178 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002179 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002180 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002181 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002182
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002183 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002184 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002185 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002186
tandrii5d48c322016-08-18 16:19:37 -07002187 @classmethod
2188 def PatchsetConfigKey(cls):
2189 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190
tandrii5d48c322016-08-18 16:19:37 -07002191 @classmethod
2192 def CodereviewServerConfigKey(cls):
2193 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002194
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002195 def GetRieveldObjForPresubmit(self):
2196 return self.RpcServer()
2197
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002198 def SetCQState(self, new_state):
2199 props = self.GetIssueProperties()
2200 if props.get('private'):
2201 DieWithError('Cannot set-commit on private issue')
2202
2203 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002204 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002205 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002206 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002207 else:
tandrii4b233bd2016-07-06 03:50:29 -07002208 assert new_state == _CQState.DRY_RUN
2209 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002210
2211
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002212 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2213 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002214 # PatchIssue should never be called with a dirty tree. It is up to the
2215 # caller to check this, but just in case we assert here since the
2216 # consequences of the caller not checking this could be dire.
2217 assert(not git_common.is_dirty_git_tree('apply'))
2218 assert(parsed_issue_arg.valid)
2219 self._changelist.issue = parsed_issue_arg.issue
2220 if parsed_issue_arg.hostname:
2221 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2222
skobes6468b902016-10-24 08:45:10 -07002223 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2224 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2225 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002226 try:
skobes6468b902016-10-24 08:45:10 -07002227 scm_obj.apply_patch(patchset_object)
2228 except Exception as e:
2229 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002230 return 1
2231
2232 # If we had an issue, commit the current state and register the issue.
2233 if not nocommit:
2234 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2235 'patch from issue %(i)s at patchset '
2236 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2237 % {'i': self.GetIssue(), 'p': patchset})])
2238 self.SetIssue(self.GetIssue())
2239 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002240 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002241 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002242 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002243 return 0
2244
2245 @staticmethod
2246 def ParseIssueURL(parsed_url):
2247 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2248 return None
wychen3c1c1722016-08-04 11:46:36 -07002249 # Rietveld patch: https://domain/<number>/#ps<patchset>
2250 match = re.match(r'/(\d+)/$', parsed_url.path)
2251 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2252 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002253 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002254 issue=int(match.group(1)),
2255 patchset=int(match2.group(1)),
2256 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002257 # Typical url: https://domain/<issue_number>[/[other]]
2258 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2259 if match:
skobes6468b902016-10-24 08:45:10 -07002260 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002261 issue=int(match.group(1)),
2262 hostname=parsed_url.netloc)
2263 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2264 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2265 if match:
skobes6468b902016-10-24 08:45:10 -07002266 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002267 issue=int(match.group(1)),
2268 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002269 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002270 return None
2271
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002272 def CMDUploadChange(self, options, args, change):
2273 """Upload the patch to Rietveld."""
2274 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2275 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002276 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2277 if options.emulate_svn_auto_props:
2278 upload_args.append('--emulate_svn_auto_props')
2279
2280 change_desc = None
2281
2282 if options.email is not None:
2283 upload_args.extend(['--email', options.email])
2284
2285 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002286 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002287 upload_args.extend(['--title', options.title])
2288 if options.message:
2289 upload_args.extend(['--message', options.message])
2290 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002291 print('This branch is associated with issue %s. '
2292 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002293 else:
nodirca166002016-06-27 10:59:51 -07002294 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002295 upload_args.extend(['--title', options.title])
2296 message = (options.title or options.message or
2297 CreateDescriptionFromLog(args))
2298 change_desc = ChangeDescription(message)
2299 if options.reviewers or options.tbr_owners:
2300 change_desc.update_reviewers(options.reviewers,
2301 options.tbr_owners,
2302 change)
2303 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002304 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002305
2306 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002307 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002308 return 1
2309
2310 upload_args.extend(['--message', change_desc.description])
2311 if change_desc.get_reviewers():
2312 upload_args.append('--reviewers=%s' % ','.join(
2313 change_desc.get_reviewers()))
2314 if options.send_mail:
2315 if not change_desc.get_reviewers():
2316 DieWithError("Must specify reviewers to send email.")
2317 upload_args.append('--send_mail')
2318
2319 # We check this before applying rietveld.private assuming that in
2320 # rietveld.cc only addresses which we can send private CLs to are listed
2321 # if rietveld.private is set, and so we should ignore rietveld.cc only
2322 # when --private is specified explicitly on the command line.
2323 if options.private:
2324 logging.warn('rietveld.cc is ignored since private flag is specified. '
2325 'You need to review and add them manually if necessary.')
2326 cc = self.GetCCListWithoutDefault()
2327 else:
2328 cc = self.GetCCList()
2329 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002330 if change_desc.get_cced():
2331 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002332 if cc:
2333 upload_args.extend(['--cc', cc])
2334
2335 if options.private or settings.GetDefaultPrivateFlag() == "True":
2336 upload_args.append('--private')
2337
2338 upload_args.extend(['--git_similarity', str(options.similarity)])
2339 if not options.find_copies:
2340 upload_args.extend(['--git_no_find_copies'])
2341
2342 # Include the upstream repo's URL in the change -- this is useful for
2343 # projects that have their source spread across multiple repos.
2344 remote_url = self.GetGitBaseUrlFromConfig()
2345 if not remote_url:
2346 if settings.GetIsGitSvn():
2347 remote_url = self.GetGitSvnRemoteUrl()
2348 else:
2349 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2350 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2351 self.GetUpstreamBranch().split('/')[-1])
2352 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002353 remote, remote_branch = self.GetRemoteBranch()
2354 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2355 settings.GetPendingRefPrefix())
2356 if target_ref:
2357 upload_args.extend(['--target_ref', target_ref])
2358
2359 # Look for dependent patchsets. See crbug.com/480453 for more details.
2360 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2361 upstream_branch = ShortBranchName(upstream_branch)
2362 if remote is '.':
2363 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002364 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002365 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002366 print()
2367 print('Skipping dependency patchset upload because git config '
2368 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2369 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002370 else:
2371 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002372 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002373 auth_config=auth_config)
2374 branch_cl_issue_url = branch_cl.GetIssueURL()
2375 branch_cl_issue = branch_cl.GetIssue()
2376 branch_cl_patchset = branch_cl.GetPatchset()
2377 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2378 upload_args.extend(
2379 ['--depends_on_patchset', '%s:%s' % (
2380 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002381 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002382 '\n'
2383 'The current branch (%s) is tracking a local branch (%s) with '
2384 'an associated CL.\n'
2385 'Adding %s/#ps%s as a dependency patchset.\n'
2386 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2387 branch_cl_patchset))
2388
2389 project = settings.GetProject()
2390 if project:
2391 upload_args.extend(['--project', project])
2392
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002393 try:
2394 upload_args = ['upload'] + upload_args + args
2395 logging.info('upload.RealMain(%s)', upload_args)
2396 issue, patchset = upload.RealMain(upload_args)
2397 issue = int(issue)
2398 patchset = int(patchset)
2399 except KeyboardInterrupt:
2400 sys.exit(1)
2401 except:
2402 # If we got an exception after the user typed a description for their
2403 # change, back up the description before re-raising.
2404 if change_desc:
2405 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2406 print('\nGot exception while uploading -- saving description to %s\n' %
2407 backup_path)
2408 backup_file = open(backup_path, 'w')
2409 backup_file.write(change_desc.description)
2410 backup_file.close()
2411 raise
2412
2413 if not self.GetIssue():
2414 self.SetIssue(issue)
2415 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002416 return 0
2417
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002418
2419class _GerritChangelistImpl(_ChangelistCodereviewBase):
2420 def __init__(self, changelist, auth_config=None):
2421 # auth_config is Rietveld thing, kept here to preserve interface only.
2422 super(_GerritChangelistImpl, self).__init__(changelist)
2423 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002424 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002425 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002426 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002427
2428 def _GetGerritHost(self):
2429 # Lazy load of configs.
2430 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002431 if self._gerrit_host and '.' not in self._gerrit_host:
2432 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2433 # This happens for internal stuff http://crbug.com/614312.
2434 parsed = urlparse.urlparse(self.GetRemoteUrl())
2435 if parsed.scheme == 'sso':
2436 print('WARNING: using non https URLs for remote is likely broken\n'
2437 ' Your current remote is: %s' % self.GetRemoteUrl())
2438 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2439 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002440 return self._gerrit_host
2441
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002442 def _GetGitHost(self):
2443 """Returns git host to be used when uploading change to Gerrit."""
2444 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2445
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002446 def GetCodereviewServer(self):
2447 if not self._gerrit_server:
2448 # If we're on a branch then get the server potentially associated
2449 # with that branch.
2450 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002451 self._gerrit_server = self._GitGetBranchConfigValue(
2452 self.CodereviewServerConfigKey())
2453 if self._gerrit_server:
2454 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002455 if not self._gerrit_server:
2456 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2457 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002458 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002459 parts[0] = parts[0] + '-review'
2460 self._gerrit_host = '.'.join(parts)
2461 self._gerrit_server = 'https://%s' % self._gerrit_host
2462 return self._gerrit_server
2463
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002464 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002465 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002466 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002467
tandrii5d48c322016-08-18 16:19:37 -07002468 @classmethod
2469 def PatchsetConfigKey(cls):
2470 return 'gerritpatchset'
2471
2472 @classmethod
2473 def CodereviewServerConfigKey(cls):
2474 return 'gerritserver'
2475
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002476 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002477 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002478 if settings.GetGerritSkipEnsureAuthenticated():
2479 # For projects with unusual authentication schemes.
2480 # See http://crbug.com/603378.
2481 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002482 # Lazy-loader to identify Gerrit and Git hosts.
2483 if gerrit_util.GceAuthenticator.is_gce():
2484 return
2485 self.GetCodereviewServer()
2486 git_host = self._GetGitHost()
2487 assert self._gerrit_server and self._gerrit_host
2488 cookie_auth = gerrit_util.CookiesAuthenticator()
2489
2490 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2491 git_auth = cookie_auth.get_auth_header(git_host)
2492 if gerrit_auth and git_auth:
2493 if gerrit_auth == git_auth:
2494 return
2495 print((
2496 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2497 ' Check your %s or %s file for credentials of hosts:\n'
2498 ' %s\n'
2499 ' %s\n'
2500 ' %s') %
2501 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2502 git_host, self._gerrit_host,
2503 cookie_auth.get_new_password_message(git_host)))
2504 if not force:
2505 ask_for_data('If you know what you are doing, press Enter to continue, '
2506 'Ctrl+C to abort.')
2507 return
2508 else:
2509 missing = (
2510 [] if gerrit_auth else [self._gerrit_host] +
2511 [] if git_auth else [git_host])
2512 DieWithError('Credentials for the following hosts are required:\n'
2513 ' %s\n'
2514 'These are read from %s (or legacy %s)\n'
2515 '%s' % (
2516 '\n '.join(missing),
2517 cookie_auth.get_gitcookies_path(),
2518 cookie_auth.get_netrc_path(),
2519 cookie_auth.get_new_password_message(git_host)))
2520
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002521 def _PostUnsetIssueProperties(self):
2522 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002523 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002524
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002525 def GetRieveldObjForPresubmit(self):
2526 class ThisIsNotRietveldIssue(object):
2527 def __nonzero__(self):
2528 # This is a hack to make presubmit_support think that rietveld is not
2529 # defined, yet still ensure that calls directly result in a decent
2530 # exception message below.
2531 return False
2532
2533 def __getattr__(self, attr):
2534 print(
2535 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2536 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2537 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2538 'or use Rietveld for codereview.\n'
2539 'See also http://crbug.com/579160.' % attr)
2540 raise NotImplementedError()
2541 return ThisIsNotRietveldIssue()
2542
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002543 def GetGerritObjForPresubmit(self):
2544 return presubmit_support.GerritAccessor(self._GetGerritHost())
2545
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002546 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002547 """Apply a rough heuristic to give a simple summary of an issue's review
2548 or CQ status, assuming adherence to a common workflow.
2549
2550 Returns None if no issue for this branch, or one of the following keywords:
2551 * 'error' - error from review tool (including deleted issues)
2552 * 'unsent' - no reviewers added
2553 * 'waiting' - waiting for review
2554 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002555 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2556 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002557 * 'commit' - in the commit queue
2558 * 'closed' - abandoned
2559 """
2560 if not self.GetIssue():
2561 return None
2562
2563 try:
2564 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002565 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002566 return 'error'
2567
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002568 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002569 return 'closed'
2570
2571 cq_label = data['labels'].get('Commit-Queue', {})
2572 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002573 votes = cq_label.get('all', [])
2574 highest_vote = 0
2575 for v in votes:
2576 highest_vote = max(highest_vote, v.get('value', 0))
2577 vote_value = str(highest_vote)
2578 if vote_value != '0':
2579 # Add a '+' if the value is not 0 to match the values in the label.
2580 # The cq_label does not have negatives.
2581 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002582 vote_text = cq_label.get('values', {}).get(vote_value, '')
2583 if vote_text.lower() == 'commit':
2584 return 'commit'
2585
2586 lgtm_label = data['labels'].get('Code-Review', {})
2587 if lgtm_label:
2588 if 'rejected' in lgtm_label:
2589 return 'not lgtm'
2590 if 'approved' in lgtm_label:
2591 return 'lgtm'
2592
2593 if not data.get('reviewers', {}).get('REVIEWER', []):
2594 return 'unsent'
2595
2596 messages = data.get('messages', [])
2597 if messages:
2598 owner = data['owner'].get('_account_id')
2599 last_message_author = messages[-1].get('author', {}).get('_account_id')
2600 if owner != last_message_author:
2601 # Some reply from non-owner.
2602 return 'reply'
2603
2604 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002605
2606 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002607 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002608 return data['revisions'][data['current_revision']]['_number']
2609
2610 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002611 data = self._GetChangeDetail(['CURRENT_REVISION'])
2612 current_rev = data['current_revision']
2613 url = data['revisions'][current_rev]['fetch']['http']['url']
2614 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002615
dsansomee2d6fd92016-09-08 00:10:47 -07002616 def UpdateDescriptionRemote(self, description, force=False):
2617 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2618 if not force:
2619 ask_for_data(
2620 'The description cannot be modified while the issue has a pending '
2621 'unpublished edit. Either publish the edit in the Gerrit web UI '
2622 'or delete it.\n\n'
2623 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2624
2625 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2626 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002627 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002628 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002629
2630 def CloseIssue(self):
2631 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2632
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002633 def GetApprovingReviewers(self):
2634 """Returns a list of reviewers approving the change.
2635
2636 Note: not necessarily committers.
2637 """
2638 raise NotImplementedError()
2639
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002640 def SubmitIssue(self, wait_for_merge=True):
2641 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2642 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002643
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002644 def _GetChangeDetail(self, options=None, issue=None):
2645 options = options or []
2646 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002647 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002648 try:
2649 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2650 options, ignore_404=False)
2651 except gerrit_util.GerritError as e:
2652 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002653 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002654 raise
tandriic2405f52016-10-10 08:13:15 -07002655 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002656
agable32978d92016-11-01 12:55:02 -07002657 def _GetChangeCommit(self, issue=None):
2658 issue = issue or self.GetIssue()
2659 assert issue, 'issue is required to query Gerrit'
2660 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2661 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002662 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002663 return data
2664
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002665 def CMDLand(self, force, bypass_hooks, verbose):
2666 if git_common.is_dirty_git_tree('land'):
2667 return 1
tandriid60367b2016-06-22 05:25:12 -07002668 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2669 if u'Commit-Queue' in detail.get('labels', {}):
2670 if not force:
2671 ask_for_data('\nIt seems this repository has a Commit Queue, '
2672 'which can test and land changes for you. '
2673 'Are you sure you wish to bypass it?\n'
2674 'Press Enter to continue, Ctrl+C to abort.')
2675
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002676 differs = True
tandriic4344b52016-08-29 06:04:54 -07002677 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002678 # Note: git diff outputs nothing if there is no diff.
2679 if not last_upload or RunGit(['diff', last_upload]).strip():
2680 print('WARNING: some changes from local branch haven\'t been uploaded')
2681 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002682 if detail['current_revision'] == last_upload:
2683 differs = False
2684 else:
2685 print('WARNING: local branch contents differ from latest uploaded '
2686 'patchset')
2687 if differs:
2688 if not force:
2689 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002690 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2691 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002692 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2693 elif not bypass_hooks:
2694 hook_results = self.RunHook(
2695 committing=True,
2696 may_prompt=not force,
2697 verbose=verbose,
2698 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2699 if not hook_results.should_continue():
2700 return 1
2701
2702 self.SubmitIssue(wait_for_merge=True)
2703 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002704 links = self._GetChangeCommit().get('web_links', [])
2705 for link in links:
2706 if link.get('name') == 'gerrit' and link.get('url'):
2707 print('Landed as %s' % link.get('url'))
2708 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002709 return 0
2710
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002711 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2712 directory):
2713 assert not reject
2714 assert not nocommit
2715 assert not directory
2716 assert parsed_issue_arg.valid
2717
2718 self._changelist.issue = parsed_issue_arg.issue
2719
2720 if parsed_issue_arg.hostname:
2721 self._gerrit_host = parsed_issue_arg.hostname
2722 self._gerrit_server = 'https://%s' % self._gerrit_host
2723
tandriic2405f52016-10-10 08:13:15 -07002724 try:
2725 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002726 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002727 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002728
2729 if not parsed_issue_arg.patchset:
2730 # Use current revision by default.
2731 revision_info = detail['revisions'][detail['current_revision']]
2732 patchset = int(revision_info['_number'])
2733 else:
2734 patchset = parsed_issue_arg.patchset
2735 for revision_info in detail['revisions'].itervalues():
2736 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2737 break
2738 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002739 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002740 (parsed_issue_arg.patchset, self.GetIssue()))
2741
2742 fetch_info = revision_info['fetch']['http']
2743 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2744 RunGit(['cherry-pick', 'FETCH_HEAD'])
2745 self.SetIssue(self.GetIssue())
2746 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002747 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002748 (self.GetIssue(), self.GetPatchset()))
2749 return 0
2750
2751 @staticmethod
2752 def ParseIssueURL(parsed_url):
2753 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2754 return None
2755 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2756 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2757 # Short urls like https://domain/<issue_number> can be used, but don't allow
2758 # specifying the patchset (you'd 404), but we allow that here.
2759 if parsed_url.path == '/':
2760 part = parsed_url.fragment
2761 else:
2762 part = parsed_url.path
2763 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2764 if match:
2765 return _ParsedIssueNumberArgument(
2766 issue=int(match.group(2)),
2767 patchset=int(match.group(4)) if match.group(4) else None,
2768 hostname=parsed_url.netloc)
2769 return None
2770
tandrii16e0b4e2016-06-07 10:34:28 -07002771 def _GerritCommitMsgHookCheck(self, offer_removal):
2772 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2773 if not os.path.exists(hook):
2774 return
2775 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2776 # custom developer made one.
2777 data = gclient_utils.FileRead(hook)
2778 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2779 return
2780 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002781 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002782 'and may interfere with it in subtle ways.\n'
2783 'We recommend you remove the commit-msg hook.')
2784 if offer_removal:
2785 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2786 if reply.lower().startswith('y'):
2787 gclient_utils.rm_file_or_tree(hook)
2788 print('Gerrit commit-msg hook removed.')
2789 else:
2790 print('OK, will keep Gerrit commit-msg hook in place.')
2791
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002792 def CMDUploadChange(self, options, args, change):
2793 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002794 if options.squash and options.no_squash:
2795 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002796
2797 if not options.squash and not options.no_squash:
2798 # Load default for user, repo, squash=true, in this order.
2799 options.squash = settings.GetSquashGerritUploads()
2800 elif options.no_squash:
2801 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002802
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002803 # We assume the remote called "origin" is the one we want.
2804 # It is probably not worthwhile to support different workflows.
2805 gerrit_remote = 'origin'
2806
2807 remote, remote_branch = self.GetRemoteBranch()
2808 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2809 pending_prefix='')
2810
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002811 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002812 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813 if self.GetIssue():
2814 # Try to get the message from a previous upload.
2815 message = self.GetDescription()
2816 if not message:
2817 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002818 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002819 '%s' % (self.GetIssue(), self.GetIssueURL()))
2820 change_id = self._GetChangeDetail()['change_id']
2821 while True:
2822 footer_change_ids = git_footers.get_footer_change_id(message)
2823 if footer_change_ids == [change_id]:
2824 break
2825 if not footer_change_ids:
2826 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002827 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002828 continue
2829 # There is already a valid footer but with different or several ids.
2830 # Doing this automatically is non-trivial as we don't want to lose
2831 # existing other footers, yet we want to append just 1 desired
2832 # Change-Id. Thus, just create a new footer, but let user verify the
2833 # new description.
2834 message = '%s\n\nChange-Id: %s' % (message, change_id)
2835 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002836 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002837 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002838 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002839 'Please, check the proposed correction to the description, '
2840 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2841 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2842 change_id))
2843 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2844 if not options.force:
2845 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002846 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002847 message = change_desc.description
2848 if not message:
2849 DieWithError("Description is empty. Aborting...")
2850 # Continue the while loop.
2851 # Sanity check of this code - we should end up with proper message
2852 # footer.
2853 assert [change_id] == git_footers.get_footer_change_id(message)
2854 change_desc = ChangeDescription(message)
2855 else:
2856 change_desc = ChangeDescription(
2857 options.message or CreateDescriptionFromLog(args))
2858 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002859 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002860 if not change_desc.description:
2861 DieWithError("Description is empty. Aborting...")
2862 message = change_desc.description
2863 change_ids = git_footers.get_footer_change_id(message)
2864 if len(change_ids) > 1:
2865 DieWithError('too many Change-Id footers, at most 1 allowed.')
2866 if not change_ids:
2867 # Generate the Change-Id automatically.
2868 message = git_footers.add_footer_change_id(
2869 message, GenerateGerritChangeId(message))
2870 change_desc.set_description(message)
2871 change_ids = git_footers.get_footer_change_id(message)
2872 assert len(change_ids) == 1
2873 change_id = change_ids[0]
2874
2875 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2876 if remote is '.':
2877 # If our upstream branch is local, we base our squashed commit on its
2878 # squashed version.
2879 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2880 # Check the squashed hash of the parent.
2881 parent = RunGit(['config',
2882 'branch.%s.gerritsquashhash' % upstream_branch_name],
2883 error_ok=True).strip()
2884 # Verify that the upstream branch has been uploaded too, otherwise
2885 # Gerrit will create additional CLs when uploading.
2886 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2887 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002888 DieWithError(
2889 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002890 'Note: maybe you\'ve uploaded it with --no-squash. '
2891 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002892 ' git cl upload --squash\n' % upstream_branch_name)
2893 else:
2894 parent = self.GetCommonAncestorWithUpstream()
2895
2896 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2897 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2898 '-m', message]).strip()
2899 else:
2900 change_desc = ChangeDescription(
2901 options.message or CreateDescriptionFromLog(args))
2902 if not change_desc.description:
2903 DieWithError("Description is empty. Aborting...")
2904
2905 if not git_footers.get_footer_change_id(change_desc.description):
2906 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002907 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2908 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002909 ref_to_push = 'HEAD'
2910 parent = '%s/%s' % (gerrit_remote, branch)
2911 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2912
2913 assert change_desc
2914 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2915 ref_to_push)]).splitlines()
2916 if len(commits) > 1:
2917 print('WARNING: This will upload %d commits. Run the following command '
2918 'to see which commits will be uploaded: ' % len(commits))
2919 print('git log %s..%s' % (parent, ref_to_push))
2920 print('You can also use `git squash-branch` to squash these into a '
2921 'single commit.')
2922 ask_for_data('About to upload; enter to confirm.')
2923
2924 if options.reviewers or options.tbr_owners:
2925 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2926 change)
2927
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002928 # Extra options that can be specified at push time. Doc:
2929 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2930 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002931 if change_desc.get_reviewers(tbr_only=True):
2932 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2933 refspec_opts.append('l=Code-Review+1')
2934
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002935 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002936 if not re.match(r'^[\w ]+$', options.title):
2937 options.title = re.sub(r'[^\w ]', '', options.title)
2938 print('WARNING: Patchset title may only contain alphanumeric chars '
2939 'and spaces. Cleaned up title:\n%s' % options.title)
2940 if not options.force:
2941 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002942 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2943 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002944 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2945
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002946 if options.send_mail:
2947 if not change_desc.get_reviewers():
2948 DieWithError('Must specify reviewers to send email.')
2949 refspec_opts.append('notify=ALL')
2950 else:
2951 refspec_opts.append('notify=NONE')
2952
tandrii99a72f22016-08-17 14:33:24 -07002953 reviewers = change_desc.get_reviewers()
2954 if reviewers:
2955 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002956
agablec6787972016-09-09 16:13:34 -07002957 if options.private:
2958 refspec_opts.append('draft')
2959
rmistry9eadede2016-09-19 11:22:43 -07002960 if options.topic:
2961 # Documentation on Gerrit topics is here:
2962 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2963 refspec_opts.append('topic=%s' % options.topic)
2964
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002965 refspec_suffix = ''
2966 if refspec_opts:
2967 refspec_suffix = '%' + ','.join(refspec_opts)
2968 assert ' ' not in refspec_suffix, (
2969 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002970 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002971
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002972 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002973 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002974 print_stdout=True,
2975 # Flush after every line: useful for seeing progress when running as
2976 # recipe.
2977 filter_fn=lambda _: sys.stdout.flush())
2978
2979 if options.squash:
2980 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2981 change_numbers = [m.group(1)
2982 for m in map(regex.match, push_stdout.splitlines())
2983 if m]
2984 if len(change_numbers) != 1:
2985 DieWithError(
2986 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2987 'Change-Id: %s') % (len(change_numbers), change_id))
2988 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002989 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002990
2991 # Add cc's from the CC_LIST and --cc flag (if any).
2992 cc = self.GetCCList().split(',')
2993 if options.cc:
2994 cc.extend(options.cc)
2995 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002996 if change_desc.get_cced():
2997 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002998 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002999 gerrit_util.AddReviewers(
tandrii88189772016-09-29 04:29:57 -07003000 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003001 return 0
3002
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003003 def _AddChangeIdToCommitMessage(self, options, args):
3004 """Re-commits using the current message, assumes the commit hook is in
3005 place.
3006 """
3007 log_desc = options.message or CreateDescriptionFromLog(args)
3008 git_command = ['commit', '--amend', '-m', log_desc]
3009 RunGit(git_command)
3010 new_log_desc = CreateDescriptionFromLog(args)
3011 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003012 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003013 return new_log_desc
3014 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003015 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003016
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003017 def SetCQState(self, new_state):
3018 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003019 vote_map = {
3020 _CQState.NONE: 0,
3021 _CQState.DRY_RUN: 1,
3022 _CQState.COMMIT : 2,
3023 }
3024 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3025 labels={'Commit-Queue': vote_map[new_state]})
3026
tandriie113dfd2016-10-11 10:20:12 -07003027 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003028 try:
3029 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003030 except GerritChangeNotExists:
3031 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003032
3033 if data['status'] in ('ABANDONED', 'MERGED'):
3034 return 'CL %s is closed' % self.GetIssue()
3035
3036 def GetTryjobProperties(self, patchset=None):
3037 """Returns dictionary of properties to launch tryjob."""
3038 data = self._GetChangeDetail(['ALL_REVISIONS'])
3039 patchset = int(patchset or self.GetPatchset())
3040 assert patchset
3041 revision_data = None # Pylint wants it to be defined.
3042 for revision_data in data['revisions'].itervalues():
3043 if int(revision_data['_number']) == patchset:
3044 break
3045 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003046 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003047 (patchset, self.GetIssue()))
3048 return {
3049 'patch_issue': self.GetIssue(),
3050 'patch_set': patchset or self.GetPatchset(),
3051 'patch_project': data['project'],
3052 'patch_storage': 'gerrit',
3053 'patch_ref': revision_data['fetch']['http']['ref'],
3054 'patch_repository_url': revision_data['fetch']['http']['url'],
3055 'patch_gerrit_url': self.GetCodereviewServer(),
3056 }
tandriie113dfd2016-10-11 10:20:12 -07003057
tandriide281ae2016-10-12 06:02:30 -07003058 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003059 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003060
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003061
3062_CODEREVIEW_IMPLEMENTATIONS = {
3063 'rietveld': _RietveldChangelistImpl,
3064 'gerrit': _GerritChangelistImpl,
3065}
3066
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003067
iannuccie53c9352016-08-17 14:40:40 -07003068def _add_codereview_issue_select_options(parser, extra=""):
3069 _add_codereview_select_options(parser)
3070
3071 text = ('Operate on this issue number instead of the current branch\'s '
3072 'implicit issue.')
3073 if extra:
3074 text += ' '+extra
3075 parser.add_option('-i', '--issue', type=int, help=text)
3076
3077
3078def _process_codereview_issue_select_options(parser, options):
3079 _process_codereview_select_options(parser, options)
3080 if options.issue is not None and not options.forced_codereview:
3081 parser.error('--issue must be specified with either --rietveld or --gerrit')
3082
3083
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003084def _add_codereview_select_options(parser):
3085 """Appends --gerrit and --rietveld options to force specific codereview."""
3086 parser.codereview_group = optparse.OptionGroup(
3087 parser, 'EXPERIMENTAL! Codereview override options')
3088 parser.add_option_group(parser.codereview_group)
3089 parser.codereview_group.add_option(
3090 '--gerrit', action='store_true',
3091 help='Force the use of Gerrit for codereview')
3092 parser.codereview_group.add_option(
3093 '--rietveld', action='store_true',
3094 help='Force the use of Rietveld for codereview')
3095
3096
3097def _process_codereview_select_options(parser, options):
3098 if options.gerrit and options.rietveld:
3099 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3100 options.forced_codereview = None
3101 if options.gerrit:
3102 options.forced_codereview = 'gerrit'
3103 elif options.rietveld:
3104 options.forced_codereview = 'rietveld'
3105
3106
tandriif9aefb72016-07-01 09:06:51 -07003107def _get_bug_line_values(default_project, bugs):
3108 """Given default_project and comma separated list of bugs, yields bug line
3109 values.
3110
3111 Each bug can be either:
3112 * a number, which is combined with default_project
3113 * string, which is left as is.
3114
3115 This function may produce more than one line, because bugdroid expects one
3116 project per line.
3117
3118 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3119 ['v8:123', 'chromium:789']
3120 """
3121 default_bugs = []
3122 others = []
3123 for bug in bugs.split(','):
3124 bug = bug.strip()
3125 if bug:
3126 try:
3127 default_bugs.append(int(bug))
3128 except ValueError:
3129 others.append(bug)
3130
3131 if default_bugs:
3132 default_bugs = ','.join(map(str, default_bugs))
3133 if default_project:
3134 yield '%s:%s' % (default_project, default_bugs)
3135 else:
3136 yield default_bugs
3137 for other in sorted(others):
3138 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3139 yield other
3140
3141
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003142class ChangeDescription(object):
3143 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003144 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003145 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003146 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003147 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003148
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003149 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003150 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003151
agable@chromium.org42c20792013-09-12 17:34:49 +00003152 @property # www.logilab.org/ticket/89786
3153 def description(self): # pylint: disable=E0202
3154 return '\n'.join(self._description_lines)
3155
3156 def set_description(self, desc):
3157 if isinstance(desc, basestring):
3158 lines = desc.splitlines()
3159 else:
3160 lines = [line.rstrip() for line in desc]
3161 while lines and not lines[0]:
3162 lines.pop(0)
3163 while lines and not lines[-1]:
3164 lines.pop(-1)
3165 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003166
piman@chromium.org336f9122014-09-04 02:16:55 +00003167 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003168 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003169 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003170 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003171 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003172 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003173
agable@chromium.org42c20792013-09-12 17:34:49 +00003174 # Get the set of R= and TBR= lines and remove them from the desciption.
3175 regexp = re.compile(self.R_LINE)
3176 matches = [regexp.match(line) for line in self._description_lines]
3177 new_desc = [l for i, l in enumerate(self._description_lines)
3178 if not matches[i]]
3179 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003180
agable@chromium.org42c20792013-09-12 17:34:49 +00003181 # Construct new unified R= and TBR= lines.
3182 r_names = []
3183 tbr_names = []
3184 for match in matches:
3185 if not match:
3186 continue
3187 people = cleanup_list([match.group(2).strip()])
3188 if match.group(1) == 'TBR':
3189 tbr_names.extend(people)
3190 else:
3191 r_names.extend(people)
3192 for name in r_names:
3193 if name not in reviewers:
3194 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003195 if add_owners_tbr:
3196 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003197 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003198 all_reviewers = set(tbr_names + reviewers)
3199 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3200 all_reviewers)
3201 tbr_names.extend(owners_db.reviewers_for(missing_files,
3202 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003203 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3204 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3205
3206 # Put the new lines in the description where the old first R= line was.
3207 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3208 if 0 <= line_loc < len(self._description_lines):
3209 if new_tbr_line:
3210 self._description_lines.insert(line_loc, new_tbr_line)
3211 if new_r_line:
3212 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003213 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003214 if new_r_line:
3215 self.append_footer(new_r_line)
3216 if new_tbr_line:
3217 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003218
tandriif9aefb72016-07-01 09:06:51 -07003219 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003220 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003221 self.set_description([
3222 '# Enter a description of the change.',
3223 '# This will be displayed on the codereview site.',
3224 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003225 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003226 '--------------------',
3227 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003228
agable@chromium.org42c20792013-09-12 17:34:49 +00003229 regexp = re.compile(self.BUG_LINE)
3230 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003231 prefix = settings.GetBugPrefix()
3232 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3233 for value in values:
3234 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3235 self.append_footer('BUG=%s' % value)
3236
agable@chromium.org42c20792013-09-12 17:34:49 +00003237 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003238 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003239 if not content:
3240 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003241 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003242
3243 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003244 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3245 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003246 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003247 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003248
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003249 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003250 """Adds a footer line to the description.
3251
3252 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3253 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3254 that Gerrit footers are always at the end.
3255 """
3256 parsed_footer_line = git_footers.parse_footer(line)
3257 if parsed_footer_line:
3258 # Line is a gerrit footer in the form: Footer-Key: any value.
3259 # Thus, must be appended observing Gerrit footer rules.
3260 self.set_description(
3261 git_footers.add_footer(self.description,
3262 key=parsed_footer_line[0],
3263 value=parsed_footer_line[1]))
3264 return
3265
3266 if not self._description_lines:
3267 self._description_lines.append(line)
3268 return
3269
3270 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3271 if gerrit_footers:
3272 # git_footers.split_footers ensures that there is an empty line before
3273 # actual (gerrit) footers, if any. We have to keep it that way.
3274 assert top_lines and top_lines[-1] == ''
3275 top_lines, separator = top_lines[:-1], top_lines[-1:]
3276 else:
3277 separator = [] # No need for separator if there are no gerrit_footers.
3278
3279 prev_line = top_lines[-1] if top_lines else ''
3280 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3281 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3282 top_lines.append('')
3283 top_lines.append(line)
3284 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003285
tandrii99a72f22016-08-17 14:33:24 -07003286 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003287 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003288 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003289 reviewers = [match.group(2).strip()
3290 for match in matches
3291 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003292 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003293
bradnelsond975b302016-10-23 12:20:23 -07003294 def get_cced(self):
3295 """Retrieves the list of reviewers."""
3296 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3297 cced = [match.group(2).strip() for match in matches if match]
3298 return cleanup_list(cced)
3299
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003300 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3301 """Updates this commit description given the parent.
3302
3303 This is essentially what Gnumbd used to do.
3304 Consult https://goo.gl/WMmpDe for more details.
3305 """
3306 assert parent_msg # No, orphan branch creation isn't supported.
3307 assert parent_hash
3308 assert dest_ref
3309 parent_footer_map = git_footers.parse_footers(parent_msg)
3310 # This will also happily parse svn-position, which GnumbD is no longer
3311 # supporting. While we'd generate correct footers, the verifier plugin
3312 # installed in Gerrit will block such commit (ie git push below will fail).
3313 parent_position = git_footers.get_position(parent_footer_map)
3314
3315 # Cherry-picks may have last line obscuring their prior footers,
3316 # from git_footers perspective. This is also what Gnumbd did.
3317 cp_line = None
3318 if (self._description_lines and
3319 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3320 cp_line = self._description_lines.pop()
3321
3322 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3323
3324 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3325 # user interference with actual footers we'd insert below.
3326 for i, (k, v) in enumerate(parsed_footers):
3327 if k.startswith('Cr-'):
3328 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3329
3330 # Add Position and Lineage footers based on the parent.
3331 lineage = parent_footer_map.get('Cr-Branched-From', [])
3332 if parent_position[0] == dest_ref:
3333 # Same branch as parent.
3334 number = int(parent_position[1]) + 1
3335 else:
3336 number = 1 # New branch, and extra lineage.
3337 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3338 int(parent_position[1])))
3339
3340 parsed_footers.append(('Cr-Commit-Position',
3341 '%s@{#%d}' % (dest_ref, number)))
3342 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3343
3344 self._description_lines = top_lines
3345 if cp_line:
3346 self._description_lines.append(cp_line)
3347 if self._description_lines[-1] != '':
3348 self._description_lines.append('') # Ensure footer separator.
3349 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3350
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003351
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003352def get_approving_reviewers(props):
3353 """Retrieves the reviewers that approved a CL from the issue properties with
3354 messages.
3355
3356 Note that the list may contain reviewers that are not committer, thus are not
3357 considered by the CQ.
3358 """
3359 return sorted(
3360 set(
3361 message['sender']
3362 for message in props['messages']
3363 if message['approval'] and message['sender'] in props['reviewers']
3364 )
3365 )
3366
3367
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003368def FindCodereviewSettingsFile(filename='codereview.settings'):
3369 """Finds the given file starting in the cwd and going up.
3370
3371 Only looks up to the top of the repository unless an
3372 'inherit-review-settings-ok' file exists in the root of the repository.
3373 """
3374 inherit_ok_file = 'inherit-review-settings-ok'
3375 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003376 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003377 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3378 root = '/'
3379 while True:
3380 if filename in os.listdir(cwd):
3381 if os.path.isfile(os.path.join(cwd, filename)):
3382 return open(os.path.join(cwd, filename))
3383 if cwd == root:
3384 break
3385 cwd = os.path.dirname(cwd)
3386
3387
3388def LoadCodereviewSettingsFromFile(fileobj):
3389 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003390 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003391
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003392 def SetProperty(name, setting, unset_error_ok=False):
3393 fullname = 'rietveld.' + name
3394 if setting in keyvals:
3395 RunGit(['config', fullname, keyvals[setting]])
3396 else:
3397 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3398
tandrii48df5812016-10-17 03:55:37 -07003399 if not keyvals.get('GERRIT_HOST', False):
3400 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003401 # Only server setting is required. Other settings can be absent.
3402 # In that case, we ignore errors raised during option deletion attempt.
3403 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003404 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003405 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3406 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003407 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003408 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003409 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3410 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003411 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003412 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003413 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003414 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3415 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003416
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003417 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003418 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003419
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003420 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003421 RunGit(['config', 'gerrit.squash-uploads',
3422 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003423
tandrii@chromium.org28253532016-04-14 13:46:56 +00003424 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003425 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003426 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3427
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003428 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3429 #should be of the form
3430 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3431 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3432 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3433 keyvals['ORIGIN_URL_CONFIG']])
3434
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003435
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003436def urlretrieve(source, destination):
3437 """urllib is broken for SSL connections via a proxy therefore we
3438 can't use urllib.urlretrieve()."""
3439 with open(destination, 'w') as f:
3440 f.write(urllib2.urlopen(source).read())
3441
3442
ukai@chromium.org712d6102013-11-27 00:52:58 +00003443def hasSheBang(fname):
3444 """Checks fname is a #! script."""
3445 with open(fname) as f:
3446 return f.read(2).startswith('#!')
3447
3448
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003449# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3450def DownloadHooks(*args, **kwargs):
3451 pass
3452
3453
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003454def DownloadGerritHook(force):
3455 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003456
3457 Args:
3458 force: True to update hooks. False to install hooks if not present.
3459 """
3460 if not settings.GetIsGerrit():
3461 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003462 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003463 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3464 if not os.access(dst, os.X_OK):
3465 if os.path.exists(dst):
3466 if not force:
3467 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003468 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003469 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003470 if not hasSheBang(dst):
3471 DieWithError('Not a script: %s\n'
3472 'You need to download from\n%s\n'
3473 'into .git/hooks/commit-msg and '
3474 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003475 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3476 except Exception:
3477 if os.path.exists(dst):
3478 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003479 DieWithError('\nFailed to download hooks.\n'
3480 'You need to download from\n%s\n'
3481 'into .git/hooks/commit-msg and '
3482 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003483
3484
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003485
3486def GetRietveldCodereviewSettingsInteractively():
3487 """Prompt the user for settings."""
3488 server = settings.GetDefaultServerUrl(error_ok=True)
3489 prompt = 'Rietveld server (host[:port])'
3490 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3491 newserver = ask_for_data(prompt + ':')
3492 if not server and not newserver:
3493 newserver = DEFAULT_SERVER
3494 if newserver:
3495 newserver = gclient_utils.UpgradeToHttps(newserver)
3496 if newserver != server:
3497 RunGit(['config', 'rietveld.server', newserver])
3498
3499 def SetProperty(initial, caption, name, is_url):
3500 prompt = caption
3501 if initial:
3502 prompt += ' ("x" to clear) [%s]' % initial
3503 new_val = ask_for_data(prompt + ':')
3504 if new_val == 'x':
3505 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3506 elif new_val:
3507 if is_url:
3508 new_val = gclient_utils.UpgradeToHttps(new_val)
3509 if new_val != initial:
3510 RunGit(['config', 'rietveld.' + name, new_val])
3511
3512 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3513 SetProperty(settings.GetDefaultPrivateFlag(),
3514 'Private flag (rietveld only)', 'private', False)
3515 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3516 'tree-status-url', False)
3517 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3518 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3519 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3520 'run-post-upload-hook', False)
3521
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003522@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003524 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525
tandrii5d0a0422016-09-14 06:24:35 -07003526 print('WARNING: git cl config works for Rietveld only')
3527 # TODO(tandrii): remove this once we switch to Gerrit.
3528 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003529 parser.add_option('--activate-update', action='store_true',
3530 help='activate auto-updating [rietveld] section in '
3531 '.git/config')
3532 parser.add_option('--deactivate-update', action='store_true',
3533 help='deactivate auto-updating [rietveld] section in '
3534 '.git/config')
3535 options, args = parser.parse_args(args)
3536
3537 if options.deactivate_update:
3538 RunGit(['config', 'rietveld.autoupdate', 'false'])
3539 return
3540
3541 if options.activate_update:
3542 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3543 return
3544
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003546 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547 return 0
3548
3549 url = args[0]
3550 if not url.endswith('codereview.settings'):
3551 url = os.path.join(url, 'codereview.settings')
3552
3553 # Load code review settings and download hooks (if available).
3554 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3555 return 0
3556
3557
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003558def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003559 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003560 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3561 branch = ShortBranchName(branchref)
3562 _, args = parser.parse_args(args)
3563 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003565 return RunGit(['config', 'branch.%s.base-url' % branch],
3566 error_ok=False).strip()
3567 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003568 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003569 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3570 error_ok=False).strip()
3571
3572
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003573def color_for_status(status):
3574 """Maps a Changelist status to color, for CMDstatus and other tools."""
3575 return {
3576 'unsent': Fore.RED,
3577 'waiting': Fore.BLUE,
3578 'reply': Fore.YELLOW,
3579 'lgtm': Fore.GREEN,
3580 'commit': Fore.MAGENTA,
3581 'closed': Fore.CYAN,
3582 'error': Fore.WHITE,
3583 }.get(status, Fore.WHITE)
3584
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003585
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003586def get_cl_statuses(changes, fine_grained, max_processes=None):
3587 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003588
3589 If fine_grained is true, this will fetch CL statuses from the server.
3590 Otherwise, simply indicate if there's a matching url for the given branches.
3591
3592 If max_processes is specified, it is used as the maximum number of processes
3593 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3594 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003595
3596 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003597 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003598 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003599 upload.verbosity = 0
3600
3601 if fine_grained:
3602 # Process one branch synchronously to work through authentication, then
3603 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003604 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003605 def fetch(cl):
3606 try:
3607 return (cl, cl.GetStatus())
3608 except:
3609 # See http://crbug.com/629863.
3610 logging.exception('failed to fetch status for %s:', cl)
3611 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003612 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003613
tandriiea9514a2016-08-17 12:32:37 -07003614 changes_to_fetch = changes[1:]
3615 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003616 # Exit early if there was only one branch to fetch.
3617 return
3618
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003619 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003620 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003621 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003622 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003623
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003624 fetched_cls = set()
3625 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003626 while True:
3627 try:
3628 row = it.next(timeout=5)
3629 except multiprocessing.TimeoutError:
3630 break
3631
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003632 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003633 yield row
3634
3635 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003636 for cl in set(changes_to_fetch) - fetched_cls:
3637 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003638
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003639 else:
3640 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003641 for cl in changes:
3642 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003643
rmistry@google.com2dd99862015-06-22 12:22:18 +00003644
3645def upload_branch_deps(cl, args):
3646 """Uploads CLs of local branches that are dependents of the current branch.
3647
3648 If the local branch dependency tree looks like:
3649 test1 -> test2.1 -> test3.1
3650 -> test3.2
3651 -> test2.2 -> test3.3
3652
3653 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3654 run on the dependent branches in this order:
3655 test2.1, test3.1, test3.2, test2.2, test3.3
3656
3657 Note: This function does not rebase your local dependent branches. Use it when
3658 you make a change to the parent branch that will not conflict with its
3659 dependent branches, and you would like their dependencies updated in
3660 Rietveld.
3661 """
3662 if git_common.is_dirty_git_tree('upload-branch-deps'):
3663 return 1
3664
3665 root_branch = cl.GetBranch()
3666 if root_branch is None:
3667 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3668 'Get on a branch!')
3669 if not cl.GetIssue() or not cl.GetPatchset():
3670 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3671 'patchset dependencies without an uploaded CL.')
3672
3673 branches = RunGit(['for-each-ref',
3674 '--format=%(refname:short) %(upstream:short)',
3675 'refs/heads'])
3676 if not branches:
3677 print('No local branches found.')
3678 return 0
3679
3680 # Create a dictionary of all local branches to the branches that are dependent
3681 # on it.
3682 tracked_to_dependents = collections.defaultdict(list)
3683 for b in branches.splitlines():
3684 tokens = b.split()
3685 if len(tokens) == 2:
3686 branch_name, tracked = tokens
3687 tracked_to_dependents[tracked].append(branch_name)
3688
vapiera7fbd5a2016-06-16 09:17:49 -07003689 print()
3690 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003691 dependents = []
3692 def traverse_dependents_preorder(branch, padding=''):
3693 dependents_to_process = tracked_to_dependents.get(branch, [])
3694 padding += ' '
3695 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003696 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003697 dependents.append(dependent)
3698 traverse_dependents_preorder(dependent, padding)
3699 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003700 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003701
3702 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003703 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003704 return 0
3705
vapiera7fbd5a2016-06-16 09:17:49 -07003706 print('This command will checkout all dependent branches and run '
3707 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003708 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3709
andybons@chromium.org962f9462016-02-03 20:00:42 +00003710 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003711 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003712 args.extend(['-t', 'Updated patchset dependency'])
3713
rmistry@google.com2dd99862015-06-22 12:22:18 +00003714 # Record all dependents that failed to upload.
3715 failures = {}
3716 # Go through all dependents, checkout the branch and upload.
3717 try:
3718 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003719 print()
3720 print('--------------------------------------')
3721 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003722 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003723 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003724 try:
3725 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003727 failures[dependent_branch] = 1
3728 except: # pylint: disable=W0702
3729 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003730 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003731 finally:
3732 # Swap back to the original root branch.
3733 RunGit(['checkout', '-q', root_branch])
3734
vapiera7fbd5a2016-06-16 09:17:49 -07003735 print()
3736 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003737 for dependent_branch in dependents:
3738 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003739 print(' %s : %s' % (dependent_branch, upload_status))
3740 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003741
3742 return 0
3743
3744
kmarshall3bff56b2016-06-06 18:31:47 -07003745def CMDarchive(parser, args):
3746 """Archives and deletes branches associated with closed changelists."""
3747 parser.add_option(
3748 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003749 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003750 parser.add_option(
3751 '-f', '--force', action='store_true',
3752 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003753 parser.add_option(
3754 '-d', '--dry-run', action='store_true',
3755 help='Skip the branch tagging and removal steps.')
3756 parser.add_option(
3757 '-t', '--notags', action='store_true',
3758 help='Do not tag archived branches. '
3759 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003760
3761 auth.add_auth_options(parser)
3762 options, args = parser.parse_args(args)
3763 if args:
3764 parser.error('Unsupported args: %s' % ' '.join(args))
3765 auth_config = auth.extract_auth_config_from_options(options)
3766
3767 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3768 if not branches:
3769 return 0
3770
vapiera7fbd5a2016-06-16 09:17:49 -07003771 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003772 changes = [Changelist(branchref=b, auth_config=auth_config)
3773 for b in branches.splitlines()]
3774 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3775 statuses = get_cl_statuses(changes,
3776 fine_grained=True,
3777 max_processes=options.maxjobs)
3778 proposal = [(cl.GetBranch(),
3779 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3780 for cl, status in statuses
3781 if status == 'closed']
3782 proposal.sort()
3783
3784 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003786 return 0
3787
3788 current_branch = GetCurrentBranch()
3789
vapiera7fbd5a2016-06-16 09:17:49 -07003790 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003791 if options.notags:
3792 for next_item in proposal:
3793 print(' ' + next_item[0])
3794 else:
3795 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3796 for next_item in proposal:
3797 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003798
kmarshall9249e012016-08-23 12:02:16 -07003799 # Quit now on precondition failure or if instructed by the user, either
3800 # via an interactive prompt or by command line flags.
3801 if options.dry_run:
3802 print('\nNo changes were made (dry run).\n')
3803 return 0
3804 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003805 print('You are currently on a branch \'%s\' which is associated with a '
3806 'closed codereview issue, so archive cannot proceed. Please '
3807 'checkout another branch and run this command again.' %
3808 current_branch)
3809 return 1
kmarshall9249e012016-08-23 12:02:16 -07003810 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003811 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3812 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003813 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003814 return 1
3815
3816 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003817 if not options.notags:
3818 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003819 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003820
vapiera7fbd5a2016-06-16 09:17:49 -07003821 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003822
3823 return 0
3824
3825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003826def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003827 """Show status of changelists.
3828
3829 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003830 - Red not sent for review or broken
3831 - Blue waiting for review
3832 - Yellow waiting for you to reply to review
3833 - Green LGTM'ed
3834 - Magenta in the commit queue
3835 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003836
3837 Also see 'git cl comments'.
3838 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003839 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003840 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003841 parser.add_option('-f', '--fast', action='store_true',
3842 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003843 parser.add_option(
3844 '-j', '--maxjobs', action='store', type=int,
3845 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003846
3847 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003848 _add_codereview_issue_select_options(
3849 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003850 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003851 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003852 if args:
3853 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003854 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003855
iannuccie53c9352016-08-17 14:40:40 -07003856 if options.issue is not None and not options.field:
3857 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003859 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003860 cl = Changelist(auth_config=auth_config, issue=options.issue,
3861 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003862 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003863 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864 elif options.field == 'id':
3865 issueid = cl.GetIssue()
3866 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003867 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003868 elif options.field == 'patch':
3869 patchset = cl.GetPatchset()
3870 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003871 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003872 elif options.field == 'status':
3873 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003874 elif options.field == 'url':
3875 url = cl.GetIssueURL()
3876 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003877 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003878 return 0
3879
3880 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3881 if not branches:
3882 print('No local branch found.')
3883 return 0
3884
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003885 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003886 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003887 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003888 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003889 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003890 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003891 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003892
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003893 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003894 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3895 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3896 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003897 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003898 c, status = output.next()
3899 branch_statuses[c.GetBranch()] = status
3900 status = branch_statuses.pop(branch)
3901 url = cl.GetIssueURL()
3902 if url and (not status or status == 'error'):
3903 # The issue probably doesn't exist anymore.
3904 url += ' (broken)'
3905
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003906 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003907 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003908 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003909 color = ''
3910 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003911 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003913 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003914 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003915
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003916 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003917 print()
3918 print('Current branch:',)
3919 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003920 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003921 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003922 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003923 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003924 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003925 print('Issue description:')
3926 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003927 return 0
3928
3929
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003930def colorize_CMDstatus_doc():
3931 """To be called once in main() to add colors to git cl status help."""
3932 colors = [i for i in dir(Fore) if i[0].isupper()]
3933
3934 def colorize_line(line):
3935 for color in colors:
3936 if color in line.upper():
3937 # Extract whitespaces first and the leading '-'.
3938 indent = len(line) - len(line.lstrip(' ')) + 1
3939 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3940 return line
3941
3942 lines = CMDstatus.__doc__.splitlines()
3943 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3944
3945
phajdan.jre328cf92016-08-22 04:12:17 -07003946def write_json(path, contents):
3947 with open(path, 'w') as f:
3948 json.dump(contents, f)
3949
3950
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003951@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003952def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003953 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003954
3955 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003956 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003957 parser.add_option('-r', '--reverse', action='store_true',
3958 help='Lookup the branch(es) for the specified issues. If '
3959 'no issues are specified, all branches with mapped '
3960 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003961 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003962 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003963 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003964 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003965
dnj@chromium.org406c4402015-03-03 17:22:28 +00003966 if options.reverse:
3967 branches = RunGit(['for-each-ref', 'refs/heads',
3968 '--format=%(refname:short)']).splitlines()
3969
3970 # Reverse issue lookup.
3971 issue_branch_map = {}
3972 for branch in branches:
3973 cl = Changelist(branchref=branch)
3974 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3975 if not args:
3976 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003977 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003978 for issue in args:
3979 if not issue:
3980 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003981 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003982 print('Branch for issue number %s: %s' % (
3983 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003984 if options.json:
3985 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003986 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003987 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003988 if len(args) > 0:
3989 try:
3990 issue = int(args[0])
3991 except ValueError:
3992 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003993 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003994 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003995 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003996 if options.json:
3997 write_json(options.json, {
3998 'issue': cl.GetIssue(),
3999 'issue_url': cl.GetIssueURL(),
4000 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004001 return 0
4002
4003
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004004def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004005 """Shows or posts review comments for any changelist."""
4006 parser.add_option('-a', '--add-comment', dest='comment',
4007 help='comment to add to an issue')
4008 parser.add_option('-i', dest='issue',
4009 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00004010 parser.add_option('-j', '--json-file',
4011 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004012 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004013 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004014 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004015
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004016 issue = None
4017 if options.issue:
4018 try:
4019 issue = int(options.issue)
4020 except ValueError:
4021 DieWithError('A review issue id is expected to be a number')
4022
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00004023 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004024
4025 if options.comment:
4026 cl.AddComment(options.comment)
4027 return 0
4028
4029 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00004030 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00004031 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00004032 summary.append({
4033 'date': message['date'],
4034 'lgtm': False,
4035 'message': message['text'],
4036 'not_lgtm': False,
4037 'sender': message['sender'],
4038 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004039 if message['disapproval']:
4040 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00004041 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004042 elif message['approval']:
4043 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00004044 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004045 elif message['sender'] == data['owner_email']:
4046 color = Fore.MAGENTA
4047 else:
4048 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07004049 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004050 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07004051 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004052 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004053 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00004054 if options.json_file:
4055 with open(options.json_file, 'wb') as f:
4056 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004057 return 0
4058
4059
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004060@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004061def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004062 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004063 parser.add_option('-d', '--display', action='store_true',
4064 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004065 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004066 help='New description to set for this issue (- for stdin, '
4067 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004068 parser.add_option('-f', '--force', action='store_true',
4069 help='Delete any unpublished Gerrit edits for this issue '
4070 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004071
4072 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004073 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004074 options, args = parser.parse_args(args)
4075 _process_codereview_select_options(parser, options)
4076
4077 target_issue = None
4078 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07004079 target_issue = ParseIssueNumberArgument(args[0])
4080 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004081 parser.print_help()
4082 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004083
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004084 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004085
martiniss6eda05f2016-06-30 10:18:35 -07004086 kwargs = {
4087 'auth_config': auth_config,
4088 'codereview': options.forced_codereview,
4089 }
4090 if target_issue:
4091 kwargs['issue'] = target_issue.issue
4092 if options.forced_codereview == 'rietveld':
4093 kwargs['rietveld_server'] = target_issue.hostname
4094
4095 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004096
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004097 if not cl.GetIssue():
4098 DieWithError('This branch has no associated changelist.')
4099 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004100
smut@google.com34fb6b12015-07-13 20:03:26 +00004101 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004102 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004103 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004104
4105 if options.new_description:
4106 text = options.new_description
4107 if text == '-':
4108 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004109 elif text == '+':
4110 base_branch = cl.GetCommonAncestorWithUpstream()
4111 change = cl.GetChange(base_branch, None, local_description=True)
4112 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004113
4114 description.set_description(text)
4115 else:
4116 description.prompt()
4117
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004118 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004119 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004120 return 0
4121
4122
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004123def CreateDescriptionFromLog(args):
4124 """Pulls out the commit log to use as a base for the CL description."""
4125 log_args = []
4126 if len(args) == 1 and not args[0].endswith('.'):
4127 log_args = [args[0] + '..']
4128 elif len(args) == 1 and args[0].endswith('...'):
4129 log_args = [args[0][:-1]]
4130 elif len(args) == 2:
4131 log_args = [args[0] + '..' + args[1]]
4132 else:
4133 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004134 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004135
4136
thestig@chromium.org44202a22014-03-11 19:22:18 +00004137def CMDlint(parser, args):
4138 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004139 parser.add_option('--filter', action='append', metavar='-x,+y',
4140 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004141 auth.add_auth_options(parser)
4142 options, args = parser.parse_args(args)
4143 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004144
4145 # Access to a protected member _XX of a client class
4146 # pylint: disable=W0212
4147 try:
4148 import cpplint
4149 import cpplint_chromium
4150 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004151 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004152 return 1
4153
4154 # Change the current working directory before calling lint so that it
4155 # shows the correct base.
4156 previous_cwd = os.getcwd()
4157 os.chdir(settings.GetRoot())
4158 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004159 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004160 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4161 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004162 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004163 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004164 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004165
4166 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004167 command = args + files
4168 if options.filter:
4169 command = ['--filter=' + ','.join(options.filter)] + command
4170 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004171
4172 white_regex = re.compile(settings.GetLintRegex())
4173 black_regex = re.compile(settings.GetLintIgnoreRegex())
4174 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4175 for filename in filenames:
4176 if white_regex.match(filename):
4177 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004178 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004179 else:
4180 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4181 extra_check_functions)
4182 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004183 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004184 finally:
4185 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004186 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004187 if cpplint._cpplint_state.error_count != 0:
4188 return 1
4189 return 0
4190
4191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004192def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004193 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004194 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004196 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004197 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004198 auth.add_auth_options(parser)
4199 options, args = parser.parse_args(args)
4200 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004201
sbc@chromium.org71437c02015-04-09 19:29:40 +00004202 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004203 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204 return 1
4205
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004206 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004207 if args:
4208 base_branch = args[0]
4209 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004210 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004211 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004212
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004213 cl.RunHook(
4214 committing=not options.upload,
4215 may_prompt=False,
4216 verbose=options.verbose,
4217 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004218 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004219
4220
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004221def GenerateGerritChangeId(message):
4222 """Returns Ixxxxxx...xxx change id.
4223
4224 Works the same way as
4225 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4226 but can be called on demand on all platforms.
4227
4228 The basic idea is to generate git hash of a state of the tree, original commit
4229 message, author/committer info and timestamps.
4230 """
4231 lines = []
4232 tree_hash = RunGitSilent(['write-tree'])
4233 lines.append('tree %s' % tree_hash.strip())
4234 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4235 if code == 0:
4236 lines.append('parent %s' % parent.strip())
4237 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4238 lines.append('author %s' % author.strip())
4239 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4240 lines.append('committer %s' % committer.strip())
4241 lines.append('')
4242 # Note: Gerrit's commit-hook actually cleans message of some lines and
4243 # whitespace. This code is not doing this, but it clearly won't decrease
4244 # entropy.
4245 lines.append(message)
4246 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4247 stdin='\n'.join(lines))
4248 return 'I%s' % change_hash.strip()
4249
4250
wittman@chromium.org455dc922015-01-26 20:15:50 +00004251def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4252 """Computes the remote branch ref to use for the CL.
4253
4254 Args:
4255 remote (str): The git remote for the CL.
4256 remote_branch (str): The git remote branch for the CL.
4257 target_branch (str): The target branch specified by the user.
4258 pending_prefix (str): The pending prefix from the settings.
4259 """
4260 if not (remote and remote_branch):
4261 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004262
wittman@chromium.org455dc922015-01-26 20:15:50 +00004263 if target_branch:
4264 # Cannonicalize branch references to the equivalent local full symbolic
4265 # refs, which are then translated into the remote full symbolic refs
4266 # below.
4267 if '/' not in target_branch:
4268 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4269 else:
4270 prefix_replacements = (
4271 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4272 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4273 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4274 )
4275 match = None
4276 for regex, replacement in prefix_replacements:
4277 match = re.search(regex, target_branch)
4278 if match:
4279 remote_branch = target_branch.replace(match.group(0), replacement)
4280 break
4281 if not match:
4282 # This is a branch path but not one we recognize; use as-is.
4283 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004284 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4285 # Handle the refs that need to land in different refs.
4286 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004287
wittman@chromium.org455dc922015-01-26 20:15:50 +00004288 # Create the true path to the remote branch.
4289 # Does the following translation:
4290 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4291 # * refs/remotes/origin/master -> refs/heads/master
4292 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4293 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4294 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4295 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4296 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4297 'refs/heads/')
4298 elif remote_branch.startswith('refs/remotes/branch-heads'):
4299 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4300 # If a pending prefix exists then replace refs/ with it.
4301 if pending_prefix:
4302 remote_branch = remote_branch.replace('refs/', pending_prefix)
4303 return remote_branch
4304
4305
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004306def cleanup_list(l):
4307 """Fixes a list so that comma separated items are put as individual items.
4308
4309 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4310 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4311 """
4312 items = sum((i.split(',') for i in l), [])
4313 stripped_items = (i.strip() for i in items)
4314 return sorted(filter(None, stripped_items))
4315
4316
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004317@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004318def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004319 """Uploads the current changelist to codereview.
4320
4321 Can skip dependency patchset uploads for a branch by running:
4322 git config branch.branch_name.skip-deps-uploads True
4323 To unset run:
4324 git config --unset branch.branch_name.skip-deps-uploads
4325 Can also set the above globally by using the --global flag.
4326 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004327 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4328 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004329 parser.add_option('--bypass-watchlists', action='store_true',
4330 dest='bypass_watchlists',
4331 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004332 parser.add_option('-f', action='store_true', dest='force',
4333 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004334 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004335 parser.add_option('-b', '--bug',
4336 help='pre-populate the bug number(s) for this issue. '
4337 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004338 parser.add_option('--message-file', dest='message_file',
4339 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004340 parser.add_option('-t', dest='title',
4341 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004342 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004343 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004344 help='reviewer email addresses')
4345 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004346 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004347 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004348 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004349 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004350 parser.add_option('--emulate_svn_auto_props',
4351 '--emulate-svn-auto-props',
4352 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004353 dest="emulate_svn_auto_props",
4354 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004355 parser.add_option('-c', '--use-commit-queue', action='store_true',
4356 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004357 parser.add_option('--private', action='store_true',
4358 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004359 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004360 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004361 metavar='TARGET',
4362 help='Apply CL to remote ref TARGET. ' +
4363 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004364 parser.add_option('--squash', action='store_true',
4365 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004366 parser.add_option('--no-squash', action='store_true',
4367 help='Don\'t squash multiple commits into one ' +
4368 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004369 parser.add_option('--topic', default=None,
4370 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004371 parser.add_option('--email', default=None,
4372 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004373 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4374 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004375 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4376 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004377 help='Send the patchset to do a CQ dry run right after '
4378 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004379 parser.add_option('--dependencies', action='store_true',
4380 help='Uploads CLs of all the local branches that depend on '
4381 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004382
rmistry@google.com2dd99862015-06-22 12:22:18 +00004383 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004384 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004385 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004386 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004387 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004388 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004389 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004390
sbc@chromium.org71437c02015-04-09 19:29:40 +00004391 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004392 return 1
4393
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004394 options.reviewers = cleanup_list(options.reviewers)
4395 options.cc = cleanup_list(options.cc)
4396
tandriib80458a2016-06-23 12:20:07 -07004397 if options.message_file:
4398 if options.message:
4399 parser.error('only one of --message and --message-file allowed.')
4400 options.message = gclient_utils.FileRead(options.message_file)
4401 options.message_file = None
4402
tandrii4d0545a2016-07-06 03:56:49 -07004403 if options.cq_dry_run and options.use_commit_queue:
4404 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4405
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004406 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4407 settings.GetIsGerrit()
4408
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004409 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004410 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004411
4412
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004413def IsSubmoduleMergeCommit(ref):
4414 # When submodules are added to the repo, we expect there to be a single
4415 # non-git-svn merge commit at remote HEAD with a signature comment.
4416 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004417 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004418 return RunGit(cmd) != ''
4419
4420
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004421def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004422 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004424 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4425 upstream and closes the issue automatically and atomically.
4426
4427 Otherwise (in case of Rietveld):
4428 Squashes branch into a single commit.
Andrii Shyshkalov06a25022016-11-24 16:47:00 +01004429 Updates commit message with metadata (e.g. pointer to review).
4430 Pushes the code upstream.
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004431 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004432 """
4433 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4434 help='bypass upload presubmit hook')
4435 parser.add_option('-m', dest='message',
4436 help="override review description")
4437 parser.add_option('-f', action='store_true', dest='force',
4438 help="force yes to questions (don't prompt)")
4439 parser.add_option('-c', dest='contributor',
4440 help="external contributor for patch (appended to " +
4441 "description and used as author for git). Should be " +
4442 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004443 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004444 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004445 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004446 auth_config = auth.extract_auth_config_from_options(options)
4447
4448 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004449
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004450 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4451 if cl.IsGerrit():
4452 if options.message:
4453 # This could be implemented, but it requires sending a new patch to
4454 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4455 # Besides, Gerrit has the ability to change the commit message on submit
4456 # automatically, thus there is no need to support this option (so far?).
4457 parser.error('-m MESSAGE option is not supported for Gerrit.')
4458 if options.contributor:
4459 parser.error(
4460 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4461 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4462 'the contributor\'s "name <email>". If you can\'t upload such a '
4463 'commit for review, contact your repository admin and request'
4464 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004465 if not cl.GetIssue():
Aaron Gablea45ee112016-11-22 15:14:38 -08004466 DieWithError('You must upload the change first to Gerrit.\n'
tandrii73449b02016-09-14 06:27:24 -07004467 ' If you would rather have `git cl land` upload '
4468 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004469 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4470 options.verbose)
4471
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004472 current = cl.GetBranch()
4473 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4474 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004475 print()
4476 print('Attempting to push branch %r into another local branch!' % current)
4477 print()
4478 print('Either reparent this branch on top of origin/master:')
4479 print(' git reparent-branch --root')
4480 print()
4481 print('OR run `git rebase-update` if you think the parent branch is ')
4482 print('already committed.')
4483 print()
4484 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004485 return 1
4486
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004487 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004488 # Default to merging against our best guess of the upstream branch.
4489 args = [cl.GetUpstreamBranch()]
4490
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004491 if options.contributor:
4492 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004493 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004494 return 1
4495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004497 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004498
sbc@chromium.org71437c02015-04-09 19:29:40 +00004499 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004500 return 1
4501
4502 # This rev-list syntax means "show all commits not in my branch that
4503 # are in base_branch".
4504 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4505 base_branch]).splitlines()
4506 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004507 print('Base branch "%s" has %d commits '
4508 'not in this branch.' % (base_branch, len(upstream_commits)))
4509 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004510 return 1
4511
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004512 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004513 svn_head = None
4514 if cmd == 'dcommit' or base_has_submodules:
4515 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4516 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004517
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004518 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004519 # If the base_head is a submodule merge commit, the first parent of the
4520 # base_head should be a git-svn commit, which is what we're interested in.
4521 base_svn_head = base_branch
4522 if base_has_submodules:
4523 base_svn_head += '^1'
4524
4525 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004526 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004527 print('This branch has %d additional commits not upstreamed yet.'
4528 % len(extra_commits.splitlines()))
4529 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4530 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531 return 1
4532
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004533 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004534 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004535 author = None
4536 if options.contributor:
4537 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004538 hook_results = cl.RunHook(
4539 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004540 may_prompt=not options.force,
4541 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004542 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004543 if not hook_results.should_continue():
4544 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004545
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004546 # Check the tree status if the tree status URL is set.
4547 status = GetTreeStatus()
4548 if 'closed' == status:
4549 print('The tree is closed. Please wait for it to reopen. Use '
4550 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4551 return 1
4552 elif 'unknown' == status:
4553 print('Unable to determine tree status. Please verify manually and '
4554 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4555 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004556
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004557 change_desc = ChangeDescription(options.message)
4558 if not change_desc.description and cl.GetIssue():
4559 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004560
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004561 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004562 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004563 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004564 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004565 print('No description set.')
4566 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004567 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004568
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004569 # Keep a separate copy for the commit message, because the commit message
4570 # contains the link to the Rietveld issue, while the Rietveld message contains
4571 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004572 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004573 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004574
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004575 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004576 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004577 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004578 # after it. Add a period on a new line to circumvent this. Also add a space
4579 # before the period to make sure that Gitiles continues to correctly resolve
4580 # the URL.
4581 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004582 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004583 commit_desc.append_footer('Patch from %s.' % options.contributor)
4584
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004585 print('Description:')
4586 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004587
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004588 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004590 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004591
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004592 # We want to squash all this branch's commits into one commit with the proper
4593 # description. We do this by doing a "reset --soft" to the base branch (which
4594 # keeps the working copy the same), then dcommitting that. If origin/master
4595 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4596 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004597 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004598 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4599 # Delete the branches if they exist.
4600 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4601 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4602 result = RunGitWithCode(showref_cmd)
4603 if result[0] == 0:
4604 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004605
4606 # We might be in a directory that's present in this branch but not in the
4607 # trunk. Move up to the top of the tree so that git commands that expect a
4608 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004609 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004610 if rel_base_path:
4611 os.chdir(rel_base_path)
4612
4613 # Stuff our change into the merge branch.
4614 # We wrap in a try...finally block so if anything goes wrong,
4615 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004616 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004617 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004618 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004619 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004620 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004621 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004622 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004623 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004624 RunGit(
4625 [
4626 'commit', '--author', options.contributor,
4627 '-m', commit_desc.description,
4628 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004629 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004630 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004631 if base_has_submodules:
4632 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4633 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4634 RunGit(['checkout', CHERRY_PICK_BRANCH])
4635 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004636 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004637 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004638 mirror = settings.GetGitMirror(remote)
4639 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004640 pending_prefix = settings.GetPendingRefPrefix()
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004641
4642 if ShouldGenerateGitNumberFooters():
4643 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4644 # is no pending ref to push to?
4645 logging.debug('Adding git number footers')
4646 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4647 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4648 branch)
Andrii Shyshkalova6695812016-12-06 17:47:09 +01004649 # Ensure timestamps are monotonically increasing.
4650 timestamp = max(1 + _get_committer_timestamp(merge_base),
4651 _get_committer_timestamp('HEAD'))
4652 _git_amend_head(commit_desc.description, timestamp)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004653 change_desc = ChangeDescription(commit_desc.description)
4654 # If gnumbd is sitll ON and we ultimately push to branch with
4655 # pending_prefix, gnumbd will modify footers we've just inserted with
4656 # 'Original-', which is annoying but still technically correct.
4657
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004658 if not pending_prefix or branch.startswith(pending_prefix):
4659 # If not using refs/pending/heads/* at all, or target ref is already set
4660 # to pending, then push to the target ref directly.
Andrii Shyshkalov813ec3c2016-11-24 17:06:01 +01004661 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4662 # in practise. I really tried to create a new branch tracking
4663 # refs/pending/heads/master directly and git cl land failed long before
4664 # reaching this. Disagree? Comment on http://crbug.com/642493.
4665 if pending_prefix:
4666 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4667 'Grab your .git/config, add instructions how to reproduce '
4668 'this, and post it to http://crbug.com/642493.\n'
4669 'The first reporter gets a free "Black Swan" book from '
4670 'tandrii@\n\n')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004671 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004672 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004673 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004674 else:
4675 # Cherry-pick the change on top of pending ref and then push it.
4676 assert branch.startswith('refs/'), branch
4677 assert pending_prefix[-1] == '/', pending_prefix
4678 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004679 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004680 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004681 if retcode == 0:
4682 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004683 else:
4684 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004685 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004686 'svn', 'dcommit',
4687 '-C%s' % options.similarity,
4688 '--no-rebase', '--rmdir',
4689 ]
4690 if settings.GetForceHttpsCommitUrl():
4691 # Allow forcing https commit URLs for some projects that don't allow
4692 # committing to http URLs (like Google Code).
4693 remote_url = cl.GetGitSvnRemoteUrl()
4694 if urlparse.urlparse(remote_url).scheme == 'http':
4695 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004696 cmd_args.append('--commit-url=%s' % remote_url)
4697 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004698 if 'Committed r' in output:
4699 revision = re.match(
4700 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4701 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004702 finally:
4703 # And then swap back to the original branch and clean up.
4704 RunGit(['checkout', '-q', cl.GetBranch()])
4705 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004706 if base_has_submodules:
4707 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004708
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004709 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004710 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004711 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004712
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004713 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004714 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004715 try:
4716 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4717 # We set pushed_to_pending to False, since it made it all the way to the
4718 # real ref.
4719 pushed_to_pending = False
4720 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004721 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004722
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004723 if cl.GetIssue():
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004724 # TODO(tandrii): figure out story of to pending + git numberer.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004725 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004726 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004727 if not to_pending:
4728 if viewvc_url and revision:
4729 change_desc.append_footer(
4730 'Committed: %s%s' % (viewvc_url, revision))
4731 elif revision:
4732 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004733 print('Closing issue '
4734 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004735 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004736 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004737 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004738 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004739 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004740 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004741 if options.bypass_hooks:
4742 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4743 else:
4744 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004745 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004746
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004747 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004748 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004749 print('The commit is in the pending queue (%s).' % pending_ref)
4750 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4751 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004752
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004753 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4754 if os.path.isfile(hook):
4755 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004756
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004757 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004758
4759
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004760def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004761 print()
4762 print('Waiting for commit to be landed on %s...' % real_ref)
4763 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004764 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4765 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004766 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004767
4768 loop = 0
4769 while True:
4770 sys.stdout.write('fetching (%d)... \r' % loop)
4771 sys.stdout.flush()
4772 loop += 1
4773
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004774 if mirror:
4775 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004776 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4777 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4778 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4779 for commit in commits.splitlines():
4780 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004781 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004782 return commit
4783
4784 current_rev = to_rev
4785
4786
tandriibf429402016-09-14 07:09:12 -07004787def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004788 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4789
4790 Returns:
4791 (retcode of last operation, output log of last operation).
4792 """
4793 assert pending_ref.startswith('refs/'), pending_ref
4794 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4795 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4796 code = 0
4797 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004798 max_attempts = 3
4799 attempts_left = max_attempts
4800 while attempts_left:
4801 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004802 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004803 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004804
4805 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004806 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004807 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004808 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004809 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004810 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004811 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004812 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004813 continue
4814
4815 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004816 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004817 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004818 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004819 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004820 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4821 'the following files have merge conflicts:' % pending_ref)
4822 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4823 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004824 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004825 return code, out
4826
4827 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004828 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004829 code, out = RunGitWithCode(
4830 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4831 if code == 0:
4832 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004833 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004834 return code, out
4835
vapiera7fbd5a2016-06-16 09:17:49 -07004836 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004837 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004838 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004839 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004840 print('Fatal push error. Make sure your .netrc credentials and git '
4841 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004842 return code, out
4843
vapiera7fbd5a2016-06-16 09:17:49 -07004844 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004845 return code, out
4846
4847
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004848def IsFatalPushFailure(push_stdout):
4849 """True if retrying push won't help."""
4850 return '(prohibited by Gerrit)' in push_stdout
4851
4852
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004853@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004854def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004855 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004856 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004857 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004858 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004859 message = """This repository appears to be a git-svn mirror, but we
4860don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004861 else:
4862 message = """This doesn't appear to be an SVN repository.
4863If your project has a true, writeable git repository, you probably want to run
4864'git cl land' instead.
4865If your project has a git mirror of an upstream SVN master, you probably need
4866to run 'git svn init'.
4867
4868Using the wrong command might cause your commit to appear to succeed, and the
4869review to be closed, without actually landing upstream. If you choose to
4870proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004871 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004872 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004873 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4874 'Please let us know of this project you are committing to:'
4875 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004876 return SendUpstream(parser, args, 'dcommit')
4877
4878
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004879@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004880def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004881 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004882 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004883 print('This appears to be an SVN repository.')
4884 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004885 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004886 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004887 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004888
4889
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004890@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004891def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004892 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004893 parser.add_option('-b', dest='newbranch',
4894 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004895 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004896 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004897 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4898 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004899 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004900 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004901 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004902 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004903 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004904 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004905
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004906
4907 group = optparse.OptionGroup(
4908 parser,
4909 'Options for continuing work on the current issue uploaded from a '
4910 'different clone (e.g. different machine). Must be used independently '
4911 'from the other options. No issue number should be specified, and the '
4912 'branch must have an issue number associated with it')
4913 group.add_option('--reapply', action='store_true', dest='reapply',
4914 help='Reset the branch and reapply the issue.\n'
4915 'CAUTION: This will undo any local changes in this '
4916 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004917
4918 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004919 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004920 parser.add_option_group(group)
4921
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004922 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004923 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004924 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004925 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004926 auth_config = auth.extract_auth_config_from_options(options)
4927
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004928
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004929 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004930 if options.newbranch:
4931 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004932 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004933 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004934
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004935 cl = Changelist(auth_config=auth_config,
4936 codereview=options.forced_codereview)
4937 if not cl.GetIssue():
4938 parser.error('current branch must have an associated issue')
4939
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004940 upstream = cl.GetUpstreamBranch()
4941 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004942 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004943
4944 RunGit(['reset', '--hard', upstream])
4945 if options.pull:
4946 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004947
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004948 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4949 options.directory)
4950
4951 if len(args) != 1 or not args[0]:
4952 parser.error('Must specify issue number or url')
4953
4954 # We don't want uncommitted changes mixed up with the patch.
4955 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004956 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004957
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004958 if options.newbranch:
4959 if options.force:
4960 RunGit(['branch', '-D', options.newbranch],
4961 stderr=subprocess2.PIPE, error_ok=True)
4962 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004963 elif not GetCurrentBranch():
4964 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004965
4966 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4967
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004968 if cl.IsGerrit():
4969 if options.reject:
4970 parser.error('--reject is not supported with Gerrit codereview.')
4971 if options.nocommit:
4972 parser.error('--nocommit is not supported with Gerrit codereview.')
4973 if options.directory:
4974 parser.error('--directory is not supported with Gerrit codereview.')
4975
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004976 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004977 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004978
4979
4980def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004981 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004982 # Provide a wrapper for git svn rebase to help avoid accidental
4983 # git svn dcommit.
4984 # It's the only command that doesn't use parser at all since we just defer
4985 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004986
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004987 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004988
4989
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004990def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004991 """Fetches the tree status and returns either 'open', 'closed',
4992 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004993 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004994 if url:
4995 status = urllib2.urlopen(url).read().lower()
4996 if status.find('closed') != -1 or status == '0':
4997 return 'closed'
4998 elif status.find('open') != -1 or status == '1':
4999 return 'open'
5000 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005001 return 'unset'
5002
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005003
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005004def GetTreeStatusReason():
5005 """Fetches the tree status from a json url and returns the message
5006 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005007 url = settings.GetTreeStatusUrl()
5008 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005009 connection = urllib2.urlopen(json_url)
5010 status = json.loads(connection.read())
5011 connection.close()
5012 return status['message']
5013
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005014
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005015def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005016 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005017 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005018 status = GetTreeStatus()
5019 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005020 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005021 return 2
5022
vapiera7fbd5a2016-06-16 09:17:49 -07005023 print('The tree is %s' % status)
5024 print()
5025 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005026 if status != 'open':
5027 return 1
5028 return 0
5029
5030
maruel@chromium.org15192402012-09-06 12:38:29 +00005031def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005032 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005033 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005034 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005035 '-b', '--bot', action='append',
5036 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5037 'times to specify multiple builders. ex: '
5038 '"-b win_rel -b win_layout". See '
5039 'the try server waterfall for the builders name and the tests '
5040 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005041 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005042 '-B', '--bucket', default='',
5043 help=('Buildbucket bucket to send the try requests.'))
5044 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005045 '-m', '--master', default='',
5046 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005047 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005048 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005049 help='Revision to use for the try job; default: the revision will '
5050 'be determined by the try recipe that builder runs, which usually '
5051 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005052 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005053 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005054 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005055 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005056 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005057 '--project',
5058 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005059 'in recipe to determine to which repository or directory to '
5060 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005061 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005062 '-p', '--property', dest='properties', action='append', default=[],
5063 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005064 'key2=value2 etc. The value will be treated as '
5065 'json if decodable, or as string otherwise. '
5066 'NOTE: using this may make your try job not usable for CQ, '
5067 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005068 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005069 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5070 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005071 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005072 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005073 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005074 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005075
machenbach@chromium.org45453142015-09-15 08:45:22 +00005076 # Make sure that all properties are prop=value pairs.
5077 bad_params = [x for x in options.properties if '=' not in x]
5078 if bad_params:
5079 parser.error('Got properties with missing "=": %s' % bad_params)
5080
maruel@chromium.org15192402012-09-06 12:38:29 +00005081 if args:
5082 parser.error('Unknown arguments: %s' % args)
5083
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005084 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005085 if not cl.GetIssue():
5086 parser.error('Need to upload first')
5087
tandriie113dfd2016-10-11 10:20:12 -07005088 error_message = cl.CannotTriggerTryJobReason()
5089 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005090 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005091
borenet6c0efe62016-10-19 08:13:29 -07005092 if options.bucket and options.master:
5093 parser.error('Only one of --bucket and --master may be used.')
5094
qyearsley1fdfcb62016-10-24 13:22:03 -07005095 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005096
qyearsleydd49f942016-10-28 11:57:22 -07005097 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5098 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005099 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005100 if options.verbose:
5101 print('git cl try with no bots now defaults to CQ Dry Run.')
5102 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00005103
borenet6c0efe62016-10-19 08:13:29 -07005104 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005105 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005106 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005107 'of bot requires an initial job from a parent (usually a builder). '
5108 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005109 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005110 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005111
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005112 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005113 # TODO(tandrii): Checking local patchset against remote patchset is only
5114 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5115 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005116 print('Warning: Codereview server has newer patchsets (%s) than most '
5117 'recent upload from local checkout (%s). Did a previous upload '
5118 'fail?\n'
5119 'By default, git cl try uses the latest patchset from '
5120 'codereview, continuing to use patchset %s.\n' %
5121 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005122
tandrii568043b2016-10-11 07:49:18 -07005123 try:
borenet6c0efe62016-10-19 08:13:29 -07005124 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5125 patchset)
tandrii568043b2016-10-11 07:49:18 -07005126 except BuildbucketResponseException as ex:
5127 print('ERROR: %s' % ex)
5128 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005129 return 0
5130
5131
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005132def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005133 """Prints info about try jobs associated with current CL."""
5134 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005135 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005136 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005137 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005138 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005139 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005140 '--color', action='store_true', default=setup_color.IS_TTY,
5141 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005142 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005143 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5144 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005145 group.add_option(
5146 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005147 parser.add_option_group(group)
5148 auth.add_auth_options(parser)
5149 options, args = parser.parse_args(args)
5150 if args:
5151 parser.error('Unrecognized args: %s' % ' '.join(args))
5152
5153 auth_config = auth.extract_auth_config_from_options(options)
5154 cl = Changelist(auth_config=auth_config)
5155 if not cl.GetIssue():
5156 parser.error('Need to upload first')
5157
tandrii221ab252016-10-06 08:12:04 -07005158 patchset = options.patchset
5159 if not patchset:
5160 patchset = cl.GetMostRecentPatchset()
5161 if not patchset:
5162 parser.error('Codereview doesn\'t know about issue %s. '
5163 'No access to issue or wrong issue number?\n'
5164 'Either upload first, or pass --patchset explicitely' %
5165 cl.GetIssue())
5166
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005167 # TODO(tandrii): Checking local patchset against remote patchset is only
5168 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5169 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005170 print('Warning: Codereview server has newer patchsets (%s) than most '
5171 'recent upload from local checkout (%s). Did a previous upload '
5172 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005173 'By default, git cl try-results uses the latest patchset from '
5174 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005175 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005176 try:
tandrii221ab252016-10-06 08:12:04 -07005177 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005178 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005179 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005180 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005181 if options.json:
5182 write_try_results_json(options.json, jobs)
5183 else:
5184 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005185 return 0
5186
5187
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005188@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005189def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005190 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005191 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005192 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005193 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005194
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005195 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005196 if args:
5197 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005198 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005199 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005200 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005201 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005202
5203 # Clear configured merge-base, if there is one.
5204 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005205 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005206 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005207 return 0
5208
5209
thestig@chromium.org00858c82013-12-02 23:08:03 +00005210def CMDweb(parser, args):
5211 """Opens the current CL in the web browser."""
5212 _, args = parser.parse_args(args)
5213 if args:
5214 parser.error('Unrecognized args: %s' % ' '.join(args))
5215
5216 issue_url = Changelist().GetIssueURL()
5217 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005218 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005219 return 1
5220
5221 webbrowser.open(issue_url)
5222 return 0
5223
5224
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005225def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005226 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005227 parser.add_option('-d', '--dry-run', action='store_true',
5228 help='trigger in dry run mode')
5229 parser.add_option('-c', '--clear', action='store_true',
5230 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005231 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005232 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005233 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005234 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005235 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005236 if args:
5237 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005238 if options.dry_run and options.clear:
5239 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5240
iannuccie53c9352016-08-17 14:40:40 -07005241 cl = Changelist(auth_config=auth_config, issue=options.issue,
5242 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005243 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005244 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005245 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005246 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005247 state = _CQState.DRY_RUN
5248 else:
5249 state = _CQState.COMMIT
5250 if not cl.GetIssue():
5251 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005252 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005253 return 0
5254
5255
groby@chromium.org411034a2013-02-26 15:12:01 +00005256def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005257 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005258 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005259 auth.add_auth_options(parser)
5260 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005261 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005262 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005263 if args:
5264 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005265 cl = Changelist(auth_config=auth_config, issue=options.issue,
5266 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005267 # Ensure there actually is an issue to close.
5268 cl.GetDescription()
5269 cl.CloseIssue()
5270 return 0
5271
5272
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005273def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005274 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005275 parser.add_option(
5276 '--stat',
5277 action='store_true',
5278 dest='stat',
5279 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005280 auth.add_auth_options(parser)
5281 options, args = parser.parse_args(args)
5282 auth_config = auth.extract_auth_config_from_options(options)
5283 if args:
5284 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005285
5286 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005287 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005288 # Staged changes would be committed along with the patch from last
5289 # upload, hence counted toward the "last upload" side in the final
5290 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005291 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005292 return 1
5293
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005294 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005295 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005296 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005297 if not issue:
5298 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005299 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005300 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005301
5302 # Create a new branch based on the merge-base
5303 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005304 # Clear cached branch in cl object, to avoid overwriting original CL branch
5305 # properties.
5306 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005307 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005308 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005309 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005310 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005311 return rtn
5312
wychen@chromium.org06928532015-02-03 02:11:29 +00005313 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005314 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005315 cmd = ['git', 'diff']
5316 if options.stat:
5317 cmd.append('--stat')
5318 cmd.extend([TMP_BRANCH, branch, '--'])
5319 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005320 finally:
5321 RunGit(['checkout', '-q', branch])
5322 RunGit(['branch', '-D', TMP_BRANCH])
5323
5324 return 0
5325
5326
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005327def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005328 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005329 parser.add_option(
5330 '--no-color',
5331 action='store_true',
5332 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005333 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005334 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005335 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005336
5337 author = RunGit(['config', 'user.email']).strip() or None
5338
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005339 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005340
5341 if args:
5342 if len(args) > 1:
5343 parser.error('Unknown args')
5344 base_branch = args[0]
5345 else:
5346 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005347 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005348
5349 change = cl.GetChange(base_branch, None)
5350 return owners_finder.OwnersFinder(
5351 [f.LocalPath() for f in
5352 cl.GetChange(base_branch, None).AffectedFiles()],
5353 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005354 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005355 disable_color=options.no_color).run()
5356
5357
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005358def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005359 """Generates a diff command."""
5360 # Generate diff for the current branch's changes.
5361 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5362 upstream_commit, '--' ]
5363
5364 if args:
5365 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005366 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005367 diff_cmd.append(arg)
5368 else:
5369 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005370
5371 return diff_cmd
5372
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005373def MatchingFileType(file_name, extensions):
5374 """Returns true if the file name ends with one of the given extensions."""
5375 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005376
enne@chromium.org555cfe42014-01-29 18:21:39 +00005377@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005378def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005379 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005380 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005381 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005382 parser.add_option('--full', action='store_true',
5383 help='Reformat the full content of all touched files')
5384 parser.add_option('--dry-run', action='store_true',
5385 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005386 parser.add_option('--python', action='store_true',
5387 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005388 parser.add_option('--diff', action='store_true',
5389 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005390 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005391
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005392 # git diff generates paths against the root of the repository. Change
5393 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005394 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005395 if rel_base_path:
5396 os.chdir(rel_base_path)
5397
digit@chromium.org29e47272013-05-17 17:01:46 +00005398 # Grab the merge-base commit, i.e. the upstream commit of the current
5399 # branch when it was created or the last time it was rebased. This is
5400 # to cover the case where the user may have called "git fetch origin",
5401 # moving the origin branch to a newer commit, but hasn't rebased yet.
5402 upstream_commit = None
5403 cl = Changelist()
5404 upstream_branch = cl.GetUpstreamBranch()
5405 if upstream_branch:
5406 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5407 upstream_commit = upstream_commit.strip()
5408
5409 if not upstream_commit:
5410 DieWithError('Could not find base commit for this branch. '
5411 'Are you in detached state?')
5412
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005413 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5414 diff_output = RunGit(changed_files_cmd)
5415 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005416 # Filter out files deleted by this CL
5417 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005418
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005419 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5420 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5421 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005422 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005423
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005424 top_dir = os.path.normpath(
5425 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5426
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005427 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5428 # formatted. This is used to block during the presubmit.
5429 return_value = 0
5430
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005431 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005432 # Locate the clang-format binary in the checkout
5433 try:
5434 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005435 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005436 DieWithError(e)
5437
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005438 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005439 cmd = [clang_format_tool]
5440 if not opts.dry_run and not opts.diff:
5441 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005442 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005443 if opts.diff:
5444 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005445 else:
5446 env = os.environ.copy()
5447 env['PATH'] = str(os.path.dirname(clang_format_tool))
5448 try:
5449 script = clang_format.FindClangFormatScriptInChromiumTree(
5450 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005451 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005452 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005453
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005454 cmd = [sys.executable, script, '-p0']
5455 if not opts.dry_run and not opts.diff:
5456 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005457
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005458 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5459 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005460
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005461 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5462 if opts.diff:
5463 sys.stdout.write(stdout)
5464 if opts.dry_run and len(stdout) > 0:
5465 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005466
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005467 # Similar code to above, but using yapf on .py files rather than clang-format
5468 # on C/C++ files
5469 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005470 yapf_tool = gclient_utils.FindExecutable('yapf')
5471 if yapf_tool is None:
5472 DieWithError('yapf not found in PATH')
5473
5474 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005475 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005476 cmd = [yapf_tool]
5477 if not opts.dry_run and not opts.diff:
5478 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005479 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005480 if opts.diff:
5481 sys.stdout.write(stdout)
5482 else:
5483 # TODO(sbc): yapf --lines mode still has some issues.
5484 # https://github.com/google/yapf/issues/154
5485 DieWithError('--python currently only works with --full')
5486
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005487 # Dart's formatter does not have the nice property of only operating on
5488 # modified chunks, so hard code full.
5489 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005490 try:
5491 command = [dart_format.FindDartFmtToolInChromiumTree()]
5492 if not opts.dry_run and not opts.diff:
5493 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005494 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005495
ppi@chromium.org6593d932016-03-03 15:41:15 +00005496 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005497 if opts.dry_run and stdout:
5498 return_value = 2
5499 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005500 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5501 'found in this checkout. Files in other languages are still '
5502 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005503
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005504 # Format GN build files. Always run on full build files for canonical form.
5505 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005506 cmd = ['gn', 'format' ]
5507 if opts.dry_run or opts.diff:
5508 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005509 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005510 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5511 shell=sys.platform == 'win32',
5512 cwd=top_dir)
5513 if opts.dry_run and gn_ret == 2:
5514 return_value = 2 # Not formatted.
5515 elif opts.diff and gn_ret == 2:
5516 # TODO this should compute and print the actual diff.
5517 print("This change has GN build file diff for " + gn_diff_file)
5518 elif gn_ret != 0:
5519 # For non-dry run cases (and non-2 return values for dry-run), a
5520 # nonzero error code indicates a failure, probably because the file
5521 # doesn't parse.
5522 DieWithError("gn format failed on " + gn_diff_file +
5523 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005524
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005525 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005526
5527
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005528@subcommand.usage('<codereview url or issue id>')
5529def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005530 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005531 _, args = parser.parse_args(args)
5532
5533 if len(args) != 1:
5534 parser.print_help()
5535 return 1
5536
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005537 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005538 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005539 parser.print_help()
5540 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005541 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005542
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005543 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005544 output = RunGit(['config', '--local', '--get-regexp',
5545 r'branch\..*\.%s' % issueprefix],
5546 error_ok=True)
5547 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005548 if issue == target_issue:
5549 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005550
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005551 branches = []
5552 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005553 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005554 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005555 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005556 return 1
5557 if len(branches) == 1:
5558 RunGit(['checkout', branches[0]])
5559 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005560 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005561 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005562 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005563 which = raw_input('Choose by index: ')
5564 try:
5565 RunGit(['checkout', branches[int(which)]])
5566 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005567 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005568 return 1
5569
5570 return 0
5571
5572
maruel@chromium.org29404b52014-09-08 22:58:00 +00005573def CMDlol(parser, args):
5574 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005575 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005576 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5577 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5578 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005579 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005580 return 0
5581
5582
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005583class OptionParser(optparse.OptionParser):
5584 """Creates the option parse and add --verbose support."""
5585 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005586 optparse.OptionParser.__init__(
5587 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005588 self.add_option(
5589 '-v', '--verbose', action='count', default=0,
5590 help='Use 2 times for more debugging info')
5591
5592 def parse_args(self, args=None, values=None):
5593 options, args = optparse.OptionParser.parse_args(self, args, values)
5594 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5595 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5596 return options, args
5597
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005599def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005600 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005601 print('\nYour python version %s is unsupported, please upgrade.\n' %
5602 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005603 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005604
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005605 # Reload settings.
5606 global settings
5607 settings = Settings()
5608
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005609 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005610 dispatcher = subcommand.CommandDispatcher(__name__)
5611 try:
5612 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005613 except auth.AuthenticationError as e:
5614 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005615 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005616 if e.code != 500:
5617 raise
5618 DieWithError(
5619 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5620 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005621 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005622
5623
5624if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005625 # These affect sys.stdout so do it outside of main() to simplify mocks in
5626 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005627 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005628 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005629 try:
5630 sys.exit(main(sys.argv[1:]))
5631 except KeyboardInterrupt:
5632 sys.stderr.write('interrupted\n')
5633 sys.exit(1)