blob: 4b91f5d55a0a55184c41c4dda1139275fcca4713 [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
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
skobes6468b902016-10-24 08:45:10 -070043import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
88
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070090 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091 sys.exit(1)
92
93
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000094def GetNoGitPagerEnv():
95 env = os.environ.copy()
96 # 'cat' is a magical git string that disables pagers on all platforms.
97 env['GIT_PAGER'] = 'cat'
98 return env
99
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000100
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000103 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000104 except subprocess2.CalledProcessError as e:
105 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 'Command "%s" failed.\n%s' % (
109 ' '.join(args), error_message or e.stdout or ''))
110 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
113def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000115 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000116
117
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000119 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 try:
tandrii5d48c322016-08-18 16:19:37 -0700125 (out, _), code = subprocess2.communicate(['git'] + args,
126 env=GetNoGitPagerEnv(),
127 stdout=subprocess2.PIPE,
128 stderr=stderr)
129 return code, out
130 except subprocess2.CalledProcessError as e:
131 logging.debug('Failed running %s', args)
132 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
tandrii2a16b952016-10-19 07:09:44 -0700154def time_sleep(seconds):
155 # Use this so that it can be mocked in tests without interfering with python
156 # system machinery.
157 import time # Local import to discourage others from importing time globally.
158 return time.sleep(seconds)
159
160
maruel@chromium.org90541732011-04-01 17:54:18 +0000161def ask_for_data(prompt):
162 try:
163 return raw_input(prompt)
164 except KeyboardInterrupt:
165 # Hide the exception.
166 sys.exit(1)
167
168
tandrii5d48c322016-08-18 16:19:37 -0700169def _git_branch_config_key(branch, key):
170 """Helper method to return Git config key for a branch."""
171 assert branch, 'branch name is required to set git config for it'
172 return 'branch.%s.%s' % (branch, key)
173
174
175def _git_get_branch_config_value(key, default=None, value_type=str,
176 branch=False):
177 """Returns git config value of given or current branch if any.
178
179 Returns default in all other cases.
180 """
181 assert value_type in (int, str, bool)
182 if branch is False: # Distinguishing default arg value from None.
183 branch = GetCurrentBranch()
184
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000185 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700186 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000187
tandrii5d48c322016-08-18 16:19:37 -0700188 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700189 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700190 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700191 # git config also has --int, but apparently git config suffers from integer
192 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700193 args.append(_git_branch_config_key(branch, key))
194 code, out = RunGitWithCode(args)
195 if code == 0:
196 value = out.strip()
197 if value_type == int:
198 return int(value)
199 if value_type == bool:
200 return bool(value.lower() == 'true')
201 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 return default
203
204
tandrii5d48c322016-08-18 16:19:37 -0700205def _git_set_branch_config_value(key, value, branch=None, **kwargs):
206 """Sets the value or unsets if it's None of a git branch config.
207
208 Valid, though not necessarily existing, branch must be provided,
209 otherwise currently checked out branch is used.
210 """
211 if not branch:
212 branch = GetCurrentBranch()
213 assert branch, 'a branch name OR currently checked out branch is required'
214 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700215 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700216 if value is None:
217 args.append('--unset')
218 elif isinstance(value, bool):
219 args.append('--bool')
220 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700221 else:
tandrii33a46ff2016-08-23 05:53:40 -0700222 # git config also has --int, but apparently git config suffers from integer
223 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700224 value = str(value)
225 args.append(_git_branch_config_key(branch, key))
226 if value is not None:
227 args.append(value)
228 RunGit(args, **kwargs)
229
230
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000231def add_git_similarity(parser):
232 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700233 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000234 help='Sets the percentage that a pair of files need to match in order to'
235 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000236 parser.add_option(
237 '--find-copies', action='store_true',
238 help='Allows git to look for copies.')
239 parser.add_option(
240 '--no-find-copies', action='store_false', dest='find_copies',
241 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000242
243 old_parser_args = parser.parse_args
244 def Parse(args):
245 options, args = old_parser_args(args)
246
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000247 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.similarity = _git_get_branch_config_value(
249 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000250 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 print('Note: Saving similarity of %d%% in git config.'
252 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700253 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000254
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 options.similarity = max(0, min(options.similarity, 100))
256
257 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700258 options.find_copies = _git_get_branch_config_value(
259 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000260 else:
tandrii5d48c322016-08-18 16:19:37 -0700261 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000262
263 print('Using %d%% similarity for rename/copy detection. '
264 'Override with --similarity.' % options.similarity)
265
266 return options, args
267 parser.parse_args = Parse
268
269
machenbach@chromium.org45453142015-09-15 08:45:22 +0000270def _get_properties_from_options(options):
271 properties = dict(x.split('=', 1) for x in options.properties)
272 for key, val in properties.iteritems():
273 try:
274 properties[key] = json.loads(val)
275 except ValueError:
276 pass # If a value couldn't be evaluated, treat it as a string.
277 return properties
278
279
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000280def _prefix_master(master):
281 """Convert user-specified master name to full master name.
282
283 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
284 name, while the developers always use shortened master name
285 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
286 function does the conversion for buildbucket migration.
287 """
borenet6c0efe62016-10-19 08:13:29 -0700288 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000289 return master
borenet6c0efe62016-10-19 08:13:29 -0700290 return '%s%s' % (MASTER_PREFIX, master)
291
292
293def _unprefix_master(bucket):
294 """Convert bucket name to shortened master name.
295
296 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
297 name, while the developers always use shortened master name
298 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
299 function does the conversion for buildbucket migration.
300 """
301 if bucket.startswith(MASTER_PREFIX):
302 return bucket[len(MASTER_PREFIX):]
303 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000306def _buildbucket_retry(operation_name, http, *args, **kwargs):
307 """Retries requests to buildbucket service and returns parsed json content."""
308 try_count = 0
309 while True:
310 response, content = http.request(*args, **kwargs)
311 try:
312 content_json = json.loads(content)
313 except ValueError:
314 content_json = None
315
316 # Buildbucket could return an error even if status==200.
317 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000318 error = content_json.get('error')
319 if error.get('code') == 403:
320 raise BuildbucketResponseException(
321 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000322 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000323 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000324 raise BuildbucketResponseException(msg)
325
326 if response.status == 200:
327 if not content_json:
328 raise BuildbucketResponseException(
329 'Buildbucket returns invalid json content: %s.\n'
330 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
331 content)
332 return content_json
333 if response.status < 500 or try_count >= 2:
334 raise httplib2.HttpLib2Error(content)
335
336 # status >= 500 means transient failures.
337 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700338 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000339 try_count += 1
340 assert False, 'unreachable'
341
342
qyearsley1fdfcb62016-10-24 13:22:03 -0700343def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700344 """Returns a dict mapping bucket names to builders and tests,
345 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700346 """
qyearsleydd49f942016-10-28 11:57:22 -0700347 # If no bots are listed, we try to get a set of builders and tests based
348 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700349 if not options.bot:
350 change = changelist.GetChange(
351 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700352 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700353 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700354 change=change,
355 changed_files=change.LocalPaths(),
356 repository_root=settings.GetRoot(),
357 default_presubmit=None,
358 project=None,
359 verbose=options.verbose,
360 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700361 if masters is None:
362 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100363 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700364
qyearsley1fdfcb62016-10-24 13:22:03 -0700365 if options.bucket:
366 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700367 if options.master:
368 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700369
qyearsleydd49f942016-10-28 11:57:22 -0700370 # If bots are listed but no master or bucket, then we need to find out
371 # the corresponding master for each bot.
372 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
373 if error_message:
374 option_parser.error(
375 'Tryserver master cannot be found because: %s\n'
376 'Please manually specify the tryserver master, e.g. '
377 '"-m tryserver.chromium.linux".' % error_message)
378 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700379
380
qyearsley123a4682016-10-26 09:12:17 -0700381def _get_bucket_map_for_builders(builders):
382 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 map_url = 'https://builders-map.appspot.com/'
384 try:
qyearsley123a4682016-10-26 09:12:17 -0700385 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700386 except urllib2.URLError as e:
387 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
388 (map_url, e))
389 except ValueError as e:
390 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700391 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 return None, 'Failed to build master map.'
393
qyearsley123a4682016-10-26 09:12:17 -0700394 bucket_map = {}
395 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700396 masters = builders_map.get(builder, [])
397 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700398 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700399 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700401 (builder, masters))
402 bucket = _prefix_master(masters[0])
403 bucket_map.setdefault(bucket, {})[builder] = []
404
405 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
borenet6c0efe62016-10-19 08:13:29 -0700408def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700409 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 """Sends a request to Buildbucket to trigger try jobs for a changelist.
411
412 Args:
413 auth_config: AuthConfig for Rietveld.
414 changelist: Changelist that the try jobs are associated with.
415 buckets: A nested dict mapping bucket names to builders to tests.
416 options: Command-line options.
417 """
tandriide281ae2016-10-12 06:02:30 -0700418 assert changelist.GetIssue(), 'CL must be uploaded first'
419 codereview_url = changelist.GetCodereviewServer()
420 assert codereview_url, 'CL must be uploaded first'
421 patchset = patchset or changelist.GetMostRecentPatchset()
422 assert patchset, 'CL must be uploaded first'
423
424 codereview_host = urlparse.urlparse(codereview_url).hostname
425 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000426 http = authenticator.authorize(httplib2.Http())
427 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700428
429 # TODO(tandrii): consider caching Gerrit CL details just like
430 # _RietveldChangelistImpl does, then caching values in these two variables
431 # won't be necessary.
432 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000433
434 buildbucket_put_url = (
435 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000436 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700437 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
438 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
439 hostname=codereview_host,
440 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000441 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700442
443 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
444 shared_parameters_properties['category'] = category
445 if options.clobber:
446 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700447 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700448 if extra_properties:
449 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000450
451 batch_req_body = {'builds': []}
452 print_text = []
453 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700454 for bucket, builders_and_tests in sorted(buckets.iteritems()):
455 print_text.append('Bucket: %s' % bucket)
456 master = None
457 if bucket.startswith(MASTER_PREFIX):
458 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 for builder, tests in sorted(builders_and_tests.iteritems()):
460 print_text.append(' %s: %s' % (builder, tests))
461 parameters = {
462 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000463 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700464 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000465 'revision': options.revision,
466 }],
tandrii8c5a3532016-11-04 07:52:02 -0700467 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000469 if 'presubmit' in builder.lower():
470 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000471 if tests:
472 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700473
474 tags = [
475 'builder:%s' % builder,
476 'buildset:%s' % buildset,
477 'user_agent:git_cl_try',
478 ]
479 if master:
480 parameters['properties']['master'] = master
481 tags.append('master:%s' % master)
482
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 batch_req_body['builds'].append(
484 {
485 'bucket': bucket,
486 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000487 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700488 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489 }
490 )
491
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000492 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700493 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000494 http,
495 buildbucket_put_url,
496 'PUT',
497 body=json.dumps(batch_req_body),
498 headers={'Content-Type': 'application/json'}
499 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000500 print_text.append('To see results here, run: git cl try-results')
501 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700502 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000503
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000504
tandrii221ab252016-10-06 08:12:04 -0700505def fetch_try_jobs(auth_config, changelist, buildbucket_host,
506 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700507 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000508
qyearsley53f48a12016-09-01 10:45:13 -0700509 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 """
tandrii221ab252016-10-06 08:12:04 -0700511 assert buildbucket_host
512 assert changelist.GetIssue(), 'CL must be uploaded first'
513 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
514 patchset = patchset or changelist.GetMostRecentPatchset()
515 assert patchset, 'CL must be uploaded first'
516
517 codereview_url = changelist.GetCodereviewServer()
518 codereview_host = urlparse.urlparse(codereview_url).hostname
519 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000520 if authenticator.has_cached_credentials():
521 http = authenticator.authorize(httplib2.Http())
522 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700523 print('Warning: Some results might be missing because %s' %
524 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700525 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526 http = httplib2.Http()
527
528 http.force_exception_to_status_code = True
529
tandrii221ab252016-10-06 08:12:04 -0700530 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
531 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
532 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700534 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 params = {'tag': 'buildset:%s' % buildset}
536
537 builds = {}
538 while True:
539 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700540 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000541 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700542 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 for build in content.get('builds', []):
544 builds[build['id']] = build
545 if 'next_cursor' in content:
546 params['start_cursor'] = content['next_cursor']
547 else:
548 break
549 return builds
550
551
qyearsleyeab3c042016-08-24 09:18:28 -0700552def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 """Prints nicely result of fetch_try_jobs."""
554 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700555 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 return
557
558 # Make a copy, because we'll be modifying builds dictionary.
559 builds = builds.copy()
560 builder_names_cache = {}
561
562 def get_builder(b):
563 try:
564 return builder_names_cache[b['id']]
565 except KeyError:
566 try:
567 parameters = json.loads(b['parameters_json'])
568 name = parameters['builder_name']
569 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700570 print('WARNING: failed to get builder name for build %s: %s' % (
571 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 name = None
573 builder_names_cache[b['id']] = name
574 return name
575
576 def get_bucket(b):
577 bucket = b['bucket']
578 if bucket.startswith('master.'):
579 return bucket[len('master.'):]
580 return bucket
581
582 if options.print_master:
583 name_fmt = '%%-%ds %%-%ds' % (
584 max(len(str(get_bucket(b))) for b in builds.itervalues()),
585 max(len(str(get_builder(b))) for b in builds.itervalues()))
586 def get_name(b):
587 return name_fmt % (get_bucket(b), get_builder(b))
588 else:
589 name_fmt = '%%-%ds' % (
590 max(len(str(get_builder(b))) for b in builds.itervalues()))
591 def get_name(b):
592 return name_fmt % get_builder(b)
593
594 def sort_key(b):
595 return b['status'], b.get('result'), get_name(b), b.get('url')
596
597 def pop(title, f, color=None, **kwargs):
598 """Pop matching builds from `builds` dict and print them."""
599
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000600 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 colorize = str
602 else:
603 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
604
605 result = []
606 for b in builds.values():
607 if all(b.get(k) == v for k, v in kwargs.iteritems()):
608 builds.pop(b['id'])
609 result.append(b)
610 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700611 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700613 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000614
615 total = len(builds)
616 pop(status='COMPLETED', result='SUCCESS',
617 title='Successes:', color=Fore.GREEN,
618 f=lambda b: (get_name(b), b.get('url')))
619 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
620 title='Infra Failures:', color=Fore.MAGENTA,
621 f=lambda b: (get_name(b), b.get('url')))
622 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
623 title='Failures:', color=Fore.RED,
624 f=lambda b: (get_name(b), b.get('url')))
625 pop(status='COMPLETED', result='CANCELED',
626 title='Canceled:', color=Fore.MAGENTA,
627 f=lambda b: (get_name(b),))
628 pop(status='COMPLETED', result='FAILURE',
629 failure_reason='INVALID_BUILD_DEFINITION',
630 title='Wrong master/builder name:', color=Fore.MAGENTA,
631 f=lambda b: (get_name(b),))
632 pop(status='COMPLETED', result='FAILURE',
633 title='Other failures:',
634 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
635 pop(status='COMPLETED',
636 title='Other finished:',
637 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
638 pop(status='STARTED',
639 title='Started:', color=Fore.YELLOW,
640 f=lambda b: (get_name(b), b.get('url')))
641 pop(status='SCHEDULED',
642 title='Scheduled:',
643 f=lambda b: (get_name(b), 'id=%s' % b['id']))
644 # The last section is just in case buildbucket API changes OR there is a bug.
645 pop(title='Other:',
646 f=lambda b: (get_name(b), 'id=%s' % b['id']))
647 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700648 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000649
650
qyearsley53f48a12016-09-01 10:45:13 -0700651def write_try_results_json(output_file, builds):
652 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
653
654 The input |builds| dict is assumed to be generated by Buildbucket.
655 Buildbucket documentation: http://goo.gl/G0s101
656 """
657
658 def convert_build_dict(build):
659 return {
660 'buildbucket_id': build.get('id'),
661 'status': build.get('status'),
662 'result': build.get('result'),
663 'bucket': build.get('bucket'),
664 'builder_name': json.loads(
665 build.get('parameters_json', '{}')).get('builder_name'),
666 'failure_reason': build.get('failure_reason'),
667 'url': build.get('url'),
668 }
669
670 converted = []
671 for _, build in sorted(builds.items()):
672 converted.append(convert_build_dict(build))
673 write_json(output_file, converted)
674
675
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000676def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
677 """Return the corresponding git ref if |base_url| together with |glob_spec|
678 matches the full |url|.
679
680 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
681 """
682 fetch_suburl, as_ref = glob_spec.split(':')
683 if allow_wildcards:
684 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
685 if glob_match:
686 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
687 # "branches/{472,597,648}/src:refs/remotes/svn/*".
688 branch_re = re.escape(base_url)
689 if glob_match.group(1):
690 branch_re += '/' + re.escape(glob_match.group(1))
691 wildcard = glob_match.group(2)
692 if wildcard == '*':
693 branch_re += '([^/]*)'
694 else:
695 # Escape and replace surrounding braces with parentheses and commas
696 # with pipe symbols.
697 wildcard = re.escape(wildcard)
698 wildcard = re.sub('^\\\\{', '(', wildcard)
699 wildcard = re.sub('\\\\,', '|', wildcard)
700 wildcard = re.sub('\\\\}$', ')', wildcard)
701 branch_re += wildcard
702 if glob_match.group(3):
703 branch_re += re.escape(glob_match.group(3))
704 match = re.match(branch_re, url)
705 if match:
706 return re.sub('\*$', match.group(1), as_ref)
707
708 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
709 if fetch_suburl:
710 full_url = base_url + '/' + fetch_suburl
711 else:
712 full_url = base_url
713 if full_url == url:
714 return as_ref
715 return None
716
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000717
iannucci@chromium.org79540052012-10-19 23:15:26 +0000718def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000719 """Prints statistics about the change to the user."""
720 # --no-ext-diff is broken in some versions of Git, so try to work around
721 # this by overriding the environment (but there is still a problem if the
722 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000723 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000724 if 'GIT_EXTERNAL_DIFF' in env:
725 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000726
727 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800728 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000729 else:
730 similarity_options = ['-M%s' % similarity]
731
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000732 try:
733 stdout = sys.stdout.fileno()
734 except AttributeError:
735 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000736 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000737 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000738 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000739 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000740
741
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000742class BuildbucketResponseException(Exception):
743 pass
744
745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746class Settings(object):
747 def __init__(self):
748 self.default_server = None
749 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000750 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 self.is_git_svn = None
752 self.svn_branch = None
753 self.tree_status_url = None
754 self.viewvc_url = None
755 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000756 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000757 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000758 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000759 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000760 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000761 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000762 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763
764 def LazyUpdateIfNeeded(self):
765 """Updates the settings from a codereview.settings file, if available."""
766 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000767 # The only value that actually changes the behavior is
768 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000769 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000770 error_ok=True
771 ).strip().lower()
772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000774 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 LoadCodereviewSettingsFromFile(cr_settings_file)
776 self.updated = True
777
778 def GetDefaultServerUrl(self, error_ok=False):
779 if not self.default_server:
780 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000781 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000782 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783 if error_ok:
784 return self.default_server
785 if not self.default_server:
786 error_message = ('Could not find settings file. You must configure '
787 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000788 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000789 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 return self.default_server
791
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000792 @staticmethod
793 def GetRelativeRoot():
794 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000797 if self.root is None:
798 self.root = os.path.abspath(self.GetRelativeRoot())
799 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000801 def GetGitMirror(self, remote='origin'):
802 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000803 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000804 if not os.path.isdir(local_url):
805 return None
806 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
807 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
808 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
809 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
810 if mirror.exists():
811 return mirror
812 return None
813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 def GetIsGitSvn(self):
815 """Return true if this repo looks like it's using git-svn."""
816 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000817 if self.GetPendingRefPrefix():
818 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
819 self.is_git_svn = False
820 else:
821 # If you have any "svn-remote.*" config keys, we think you're using svn.
822 self.is_git_svn = RunGitWithCode(
823 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 return self.is_git_svn
825
826 def GetSVNBranch(self):
827 if self.svn_branch is None:
828 if not self.GetIsGitSvn():
829 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
830
831 # Try to figure out which remote branch we're based on.
832 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000833 # 1) iterate through our branch history and find the svn URL.
834 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
836 # regexp matching the git-svn line that contains the URL.
837 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
838
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000839 # We don't want to go through all of history, so read a line from the
840 # pipe at a time.
841 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000842 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000843 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
844 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000845 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000846 for line in proc.stdout:
847 match = git_svn_re.match(line)
848 if match:
849 url = match.group(1)
850 proc.stdout.close() # Cut pipe.
851 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000853 if url:
854 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
855 remotes = RunGit(['config', '--get-regexp',
856 r'^svn-remote\..*\.url']).splitlines()
857 for remote in remotes:
858 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000860 remote = match.group(1)
861 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000862 rewrite_root = RunGit(
863 ['config', 'svn-remote.%s.rewriteRoot' % remote],
864 error_ok=True).strip()
865 if rewrite_root:
866 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000867 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000868 ['config', 'svn-remote.%s.fetch' % remote],
869 error_ok=True).strip()
870 if fetch_spec:
871 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
872 if self.svn_branch:
873 break
874 branch_spec = RunGit(
875 ['config', 'svn-remote.%s.branches' % remote],
876 error_ok=True).strip()
877 if branch_spec:
878 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
879 if self.svn_branch:
880 break
881 tag_spec = RunGit(
882 ['config', 'svn-remote.%s.tags' % remote],
883 error_ok=True).strip()
884 if tag_spec:
885 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
886 if self.svn_branch:
887 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888
889 if not self.svn_branch:
890 DieWithError('Can\'t guess svn branch -- try specifying it on the '
891 'command line')
892
893 return self.svn_branch
894
895 def GetTreeStatusUrl(self, error_ok=False):
896 if not self.tree_status_url:
897 error_message = ('You must configure your tree status URL by running '
898 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000899 self.tree_status_url = self._GetRietveldConfig(
900 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000901 return self.tree_status_url
902
903 def GetViewVCUrl(self):
904 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000905 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000906 return self.viewvc_url
907
rmistry@google.com90752582014-01-14 21:04:50 +0000908 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000909 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000910
rmistry@google.com78948ed2015-07-08 23:09:57 +0000911 def GetIsSkipDependencyUpload(self, branch_name):
912 """Returns true if specified branch should skip dep uploads."""
913 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
914 error_ok=True)
915
rmistry@google.com5626a922015-02-26 14:03:30 +0000916 def GetRunPostUploadHook(self):
917 run_post_upload_hook = self._GetRietveldConfig(
918 'run-post-upload-hook', error_ok=True)
919 return run_post_upload_hook == "True"
920
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000921 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000922 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000923
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000924 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000925 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000926
ukai@chromium.orge8077812012-02-03 03:41:46 +0000927 def GetIsGerrit(self):
928 """Return true if this repo is assosiated with gerrit code review system."""
929 if self.is_gerrit is None:
930 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
931 return self.is_gerrit
932
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000933 def GetSquashGerritUploads(self):
934 """Return true if uploads to Gerrit should be squashed by default."""
935 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700936 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
937 if self.squash_gerrit_uploads is None:
938 # Default is squash now (http://crbug.com/611892#c23).
939 self.squash_gerrit_uploads = not (
940 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
941 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000942 return self.squash_gerrit_uploads
943
tandriia60502f2016-06-20 02:01:53 -0700944 def GetSquashGerritUploadsOverride(self):
945 """Return True or False if codereview.settings should be overridden.
946
947 Returns None if no override has been defined.
948 """
949 # See also http://crbug.com/611892#c23
950 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
951 error_ok=True).strip()
952 if result == 'true':
953 return True
954 if result == 'false':
955 return False
956 return None
957
tandrii@chromium.org28253532016-04-14 13:46:56 +0000958 def GetGerritSkipEnsureAuthenticated(self):
959 """Return True if EnsureAuthenticated should not be done for Gerrit
960 uploads."""
961 if self.gerrit_skip_ensure_authenticated is None:
962 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000963 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000964 error_ok=True).strip() == 'true')
965 return self.gerrit_skip_ensure_authenticated
966
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000967 def GetGitEditor(self):
968 """Return the editor specified in the git config, or None if none is."""
969 if self.git_editor is None:
970 self.git_editor = self._GetConfig('core.editor', error_ok=True)
971 return self.git_editor or None
972
thestig@chromium.org44202a22014-03-11 19:22:18 +0000973 def GetLintRegex(self):
974 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
975 DEFAULT_LINT_REGEX)
976
977 def GetLintIgnoreRegex(self):
978 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
979 DEFAULT_LINT_IGNORE_REGEX)
980
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000981 def GetProject(self):
982 if not self.project:
983 self.project = self._GetRietveldConfig('project', error_ok=True)
984 return self.project
985
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000986 def GetForceHttpsCommitUrl(self):
987 if not self.force_https_commit_url:
988 self.force_https_commit_url = self._GetRietveldConfig(
989 'force-https-commit-url', error_ok=True)
990 return self.force_https_commit_url
991
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000992 def GetPendingRefPrefix(self):
993 if not self.pending_ref_prefix:
994 self.pending_ref_prefix = self._GetRietveldConfig(
995 'pending-ref-prefix', error_ok=True)
996 return self.pending_ref_prefix
997
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000998 def _GetRietveldConfig(self, param, **kwargs):
999 return self._GetConfig('rietveld.' + param, **kwargs)
1000
rmistry@google.com78948ed2015-07-08 23:09:57 +00001001 def _GetBranchConfig(self, branch_name, param, **kwargs):
1002 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1003
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004 def _GetConfig(self, param, **kwargs):
1005 self.LazyUpdateIfNeeded()
1006 return RunGit(['config', param], **kwargs).strip()
1007
1008
Andrii Shyshkalov5fb47742016-11-24 17:09:40 +01001009def ShouldGenerateGitNumberFooters():
1010 """Decides depending on codereview.settings file in the current checkout HEAD.
1011 """
1012 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1013 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1014 cr_settings_file = FindCodereviewSettingsFile()
1015 if not cr_settings_file:
1016 return False
1017 keyvals = gclient_utils.ParseCodereviewSettingsContent(
1018 cr_settings_file.read())
Andrii Shyshkalovb8c535f2016-11-24 18:01:52 +01001019 return keyvals.get('GENERATE_GIT_NUMBER_FOOTERS', '').lower() == 'true'
Andrii Shyshkalov5fb47742016-11-24 17:09:40 +01001020
1021
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022def ShortBranchName(branch):
1023 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001024 return branch.replace('refs/heads/', '', 1)
1025
1026
1027def GetCurrentBranchRef():
1028 """Returns branch ref (e.g., refs/heads/master) or None."""
1029 return RunGit(['symbolic-ref', 'HEAD'],
1030 stderr=subprocess2.VOID, error_ok=True).strip() or None
1031
1032
1033def GetCurrentBranch():
1034 """Returns current branch or None.
1035
1036 For refs/heads/* branches, returns just last part. For others, full ref.
1037 """
1038 branchref = GetCurrentBranchRef()
1039 if branchref:
1040 return ShortBranchName(branchref)
1041 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042
1043
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001044class _CQState(object):
1045 """Enum for states of CL with respect to Commit Queue."""
1046 NONE = 'none'
1047 DRY_RUN = 'dry_run'
1048 COMMIT = 'commit'
1049
1050 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1051
1052
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001053class _ParsedIssueNumberArgument(object):
1054 def __init__(self, issue=None, patchset=None, hostname=None):
1055 self.issue = issue
1056 self.patchset = patchset
1057 self.hostname = hostname
1058
1059 @property
1060 def valid(self):
1061 return self.issue is not None
1062
1063
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001064def ParseIssueNumberArgument(arg):
1065 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1066 fail_result = _ParsedIssueNumberArgument()
1067
1068 if arg.isdigit():
1069 return _ParsedIssueNumberArgument(issue=int(arg))
1070 if not arg.startswith('http'):
1071 return fail_result
1072 url = gclient_utils.UpgradeToHttps(arg)
1073 try:
1074 parsed_url = urlparse.urlparse(url)
1075 except ValueError:
1076 return fail_result
1077 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1078 tmp = cls.ParseIssueURL(parsed_url)
1079 if tmp is not None:
1080 return tmp
1081 return fail_result
1082
1083
Aaron Gablea45ee112016-11-22 15:14:38 -08001084class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001085 def __init__(self, issue, url):
1086 self.issue = issue
1087 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001088 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001089
1090 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001091 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001092 self.issue, self.url)
1093
1094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001096 """Changelist works with one changelist in local branch.
1097
1098 Supports two codereview backends: Rietveld or Gerrit, selected at object
1099 creation.
1100
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001101 Notes:
1102 * Not safe for concurrent multi-{thread,process} use.
1103 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001104 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001105 """
1106
1107 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1108 """Create a new ChangeList instance.
1109
1110 If issue is given, the codereview must be given too.
1111
1112 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1113 Otherwise, it's decided based on current configuration of the local branch,
1114 with default being 'rietveld' for backwards compatibility.
1115 See _load_codereview_impl for more details.
1116
1117 **kwargs will be passed directly to codereview implementation.
1118 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001120 global settings
1121 if not settings:
1122 # Happens when git_cl.py is used as a utility library.
1123 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001124
1125 if issue:
1126 assert codereview, 'codereview must be known, if issue is known'
1127
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 self.branchref = branchref
1129 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001130 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 self.branch = ShortBranchName(self.branchref)
1132 else:
1133 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001135 self.lookedup_issue = False
1136 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 self.has_description = False
1138 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001141 self.cc = None
1142 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001143 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001144
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001146 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001147 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001148 assert self._codereview_impl
1149 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001150
1151 def _load_codereview_impl(self, codereview=None, **kwargs):
1152 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001153 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1154 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1155 self._codereview = codereview
1156 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001157 return
1158
1159 # Automatic selection based on issue number set for a current branch.
1160 # Rietveld takes precedence over Gerrit.
1161 assert not self.issue
1162 # Whether we find issue or not, we are doing the lookup.
1163 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001164 if self.GetBranch():
1165 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1166 issue = _git_get_branch_config_value(
1167 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1168 if issue:
1169 self._codereview = codereview
1170 self._codereview_impl = cls(self, **kwargs)
1171 self.issue = int(issue)
1172 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001173
1174 # No issue is set for this branch, so decide based on repo-wide settings.
1175 return self._load_codereview_impl(
1176 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1177 **kwargs)
1178
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001179 def IsGerrit(self):
1180 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001181
1182 def GetCCList(self):
1183 """Return the users cc'd on this CL.
1184
agable92bec4f2016-08-24 09:27:27 -07001185 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001186 """
1187 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001188 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001189 more_cc = ','.join(self.watchers)
1190 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1191 return self.cc
1192
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001193 def GetCCListWithoutDefault(self):
1194 """Return the users cc'd on this CL excluding default ones."""
1195 if self.cc is None:
1196 self.cc = ','.join(self.watchers)
1197 return self.cc
1198
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001199 def SetWatchers(self, watchers):
1200 """Set the list of email addresses that should be cc'd based on the changed
1201 files in this CL.
1202 """
1203 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204
1205 def GetBranch(self):
1206 """Returns the short branch name, e.g. 'master'."""
1207 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001208 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001209 if not branchref:
1210 return None
1211 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 self.branch = ShortBranchName(self.branchref)
1213 return self.branch
1214
1215 def GetBranchRef(self):
1216 """Returns the full branch name, e.g. 'refs/heads/master'."""
1217 self.GetBranch() # Poke the lazy loader.
1218 return self.branchref
1219
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001220 def ClearBranch(self):
1221 """Clears cached branch data of this object."""
1222 self.branch = self.branchref = None
1223
tandrii5d48c322016-08-18 16:19:37 -07001224 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1225 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1226 kwargs['branch'] = self.GetBranch()
1227 return _git_get_branch_config_value(key, default, **kwargs)
1228
1229 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1230 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1231 assert self.GetBranch(), (
1232 'this CL must have an associated branch to %sset %s%s' %
1233 ('un' if value is None else '',
1234 key,
1235 '' if value is None else ' to %r' % value))
1236 kwargs['branch'] = self.GetBranch()
1237 return _git_set_branch_config_value(key, value, **kwargs)
1238
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001239 @staticmethod
1240 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001241 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 e.g. 'origin', 'refs/heads/master'
1243 """
1244 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001245 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001248 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001250 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1251 error_ok=True).strip()
1252 if upstream_branch:
1253 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001255 # Fall back on trying a git-svn upstream branch.
1256 if settings.GetIsGitSvn():
1257 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001259 # Else, try to guess the origin remote.
1260 remote_branches = RunGit(['branch', '-r']).split()
1261 if 'origin/master' in remote_branches:
1262 # Fall back on origin/master if it exits.
1263 remote = 'origin'
1264 upstream_branch = 'refs/heads/master'
1265 elif 'origin/trunk' in remote_branches:
1266 # Fall back on origin/trunk if it exists. Generally a shared
1267 # git-svn clone
1268 remote = 'origin'
1269 upstream_branch = 'refs/heads/trunk'
1270 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001271 DieWithError(
1272 'Unable to determine default branch to diff against.\n'
1273 'Either pass complete "git diff"-style arguments, like\n'
1274 ' git cl upload origin/master\n'
1275 'or verify this branch is set up to track another \n'
1276 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277
1278 return remote, upstream_branch
1279
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001280 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001281 upstream_branch = self.GetUpstreamBranch()
1282 if not BranchExists(upstream_branch):
1283 DieWithError('The upstream for the current branch (%s) does not exist '
1284 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001285 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001286 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 def GetUpstreamBranch(self):
1289 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001292 upstream_branch = upstream_branch.replace('refs/heads/',
1293 'refs/remotes/%s/' % remote)
1294 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1295 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 self.upstream_branch = upstream_branch
1297 return self.upstream_branch
1298
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001300 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 remote, branch = None, self.GetBranch()
1302 seen_branches = set()
1303 while branch not in seen_branches:
1304 seen_branches.add(branch)
1305 remote, branch = self.FetchUpstreamTuple(branch)
1306 branch = ShortBranchName(branch)
1307 if remote != '.' or branch.startswith('refs/remotes'):
1308 break
1309 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 remotes = RunGit(['remote'], error_ok=True).split()
1311 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001313 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 logging.warning('Could not determine which remote this change is '
1316 'associated with, so defaulting to "%s". This may '
1317 'not be what you want. You may prevent this message '
1318 'by running "git svn info" as documented here: %s',
1319 self._remote,
1320 GIT_INSTRUCTIONS_URL)
1321 else:
1322 logging.warn('Could not determine which remote this change is '
1323 'associated with. You may prevent this message by '
1324 'running "git svn info" as documented here: %s',
1325 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 branch = 'HEAD'
1327 if branch.startswith('refs/remotes'):
1328 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001329 elif branch.startswith('refs/branch-heads/'):
1330 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 else:
1332 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001333 return self._remote
1334
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 def GitSanityChecks(self, upstream_git_obj):
1336 """Checks git repo status and ensures diff is from local commits."""
1337
sbc@chromium.org79706062015-01-14 21:18:12 +00001338 if upstream_git_obj is None:
1339 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001340 print('ERROR: unable to determine current branch (detached HEAD?)',
1341 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001342 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001343 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001344 return False
1345
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001346 # Verify the commit we're diffing against is in our current branch.
1347 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1348 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1349 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001350 print('ERROR: %s is not in the current branch. You may need to rebase '
1351 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001352 return False
1353
1354 # List the commits inside the diff, and verify they are all local.
1355 commits_in_diff = RunGit(
1356 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1357 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1358 remote_branch = remote_branch.strip()
1359 if code != 0:
1360 _, remote_branch = self.GetRemoteBranch()
1361
1362 commits_in_remote = RunGit(
1363 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1364
1365 common_commits = set(commits_in_diff) & set(commits_in_remote)
1366 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001367 print('ERROR: Your diff contains %d commits already in %s.\n'
1368 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1369 'the diff. If you are using a custom git flow, you can override'
1370 ' the reference used for this check with "git config '
1371 'gitcl.remotebranch <git-ref>".' % (
1372 len(common_commits), remote_branch, upstream_git_obj),
1373 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001374 return False
1375 return True
1376
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001377 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001378 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001379
1380 Returns None if it is not set.
1381 """
tandrii5d48c322016-08-18 16:19:37 -07001382 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001383
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001384 def GetGitSvnRemoteUrl(self):
1385 """Return the configured git-svn remote URL parsed from git svn info.
1386
1387 Returns None if it is not set.
1388 """
1389 # URL is dependent on the current directory.
1390 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1391 if data:
1392 keys = dict(line.split(': ', 1) for line in data.splitlines()
1393 if ': ' in line)
1394 return keys.get('URL', None)
1395 return None
1396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 def GetRemoteUrl(self):
1398 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1399
1400 Returns None if there is no remote.
1401 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001402 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001403 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1404
1405 # If URL is pointing to a local directory, it is probably a git cache.
1406 if os.path.isdir(url):
1407 url = RunGit(['config', 'remote.%s.url' % remote],
1408 error_ok=True,
1409 cwd=url).strip()
1410 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001412 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001413 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001414 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001415 self.issue = self._GitGetBranchConfigValue(
1416 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001417 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 return self.issue
1419
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 def GetIssueURL(self):
1421 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001422 issue = self.GetIssue()
1423 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001424 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001425 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426
1427 def GetDescription(self, pretty=False):
1428 if not self.has_description:
1429 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001430 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 self.has_description = True
1432 if pretty:
1433 wrapper = textwrap.TextWrapper()
1434 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1435 return wrapper.fill(self.description)
1436 return self.description
1437
1438 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001439 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001440 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001441 self.patchset = self._GitGetBranchConfigValue(
1442 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001443 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 return self.patchset
1445
1446 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001447 """Set this branch's patchset. If patchset=0, clears the patchset."""
1448 assert self.GetBranch()
1449 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001450 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001451 else:
1452 self.patchset = int(patchset)
1453 self._GitSetBranchConfigValue(
1454 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001456 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001457 """Set this branch's issue. If issue isn't given, clears the issue."""
1458 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001460 issue = int(issue)
1461 self._GitSetBranchConfigValue(
1462 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001463 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001464 codereview_server = self._codereview_impl.GetCodereviewServer()
1465 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001466 self._GitSetBranchConfigValue(
1467 self._codereview_impl.CodereviewServerConfigKey(),
1468 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469 else:
tandrii5d48c322016-08-18 16:19:37 -07001470 # Reset all of these just to be clean.
1471 reset_suffixes = [
1472 'last-upload-hash',
1473 self._codereview_impl.IssueConfigKey(),
1474 self._codereview_impl.PatchsetConfigKey(),
1475 self._codereview_impl.CodereviewServerConfigKey(),
1476 ] + self._PostUnsetIssueProperties()
1477 for prop in reset_suffixes:
1478 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001479 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001480 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481
dnjba1b0f32016-09-02 12:37:42 -07001482 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001483 if not self.GitSanityChecks(upstream_branch):
1484 DieWithError('\nGit sanity check failure')
1485
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001486 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001487 if not root:
1488 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001489 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001490
1491 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001492 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001493 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001494 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001495 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 except subprocess2.CalledProcessError:
1497 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001498 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001499 'This branch probably doesn\'t exist anymore. To reset the\n'
1500 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001501 ' git branch --set-upstream-to origin/master %s\n'
1502 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001503 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001504
maruel@chromium.org52424302012-08-29 15:14:30 +00001505 issue = self.GetIssue()
1506 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001507 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001508 description = self.GetDescription()
1509 else:
1510 # If the change was never uploaded, use the log messages of all commits
1511 # up to the branch point, as git cl upload will prefill the description
1512 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001513 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1514 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001515
1516 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001517 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001518 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001519 name,
1520 description,
1521 absroot,
1522 files,
1523 issue,
1524 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001525 author,
1526 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001527
dsansomee2d6fd92016-09-08 00:10:47 -07001528 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001529 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001530 return self._codereview_impl.UpdateDescriptionRemote(
1531 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532
1533 def RunHook(self, committing, may_prompt, verbose, change):
1534 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1535 try:
1536 return presubmit_support.DoPresubmitChecks(change, committing,
1537 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1538 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001539 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1540 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001541 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001542 DieWithError(
1543 ('%s\nMaybe your depot_tools is out of date?\n'
1544 'If all fails, contact maruel@') % e)
1545
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001546 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1547 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001548 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1549 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001550 else:
1551 # Assume url.
1552 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1553 urlparse.urlparse(issue_arg))
1554 if not parsed_issue_arg or not parsed_issue_arg.valid:
1555 DieWithError('Failed to parse issue argument "%s". '
1556 'Must be an issue number or a valid URL.' % issue_arg)
1557 return self._codereview_impl.CMDPatchWithParsedIssue(
1558 parsed_issue_arg, reject, nocommit, directory)
1559
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 def CMDUpload(self, options, git_diff_args, orig_args):
1561 """Uploads a change to codereview."""
1562 if git_diff_args:
1563 # TODO(ukai): is it ok for gerrit case?
1564 base_branch = git_diff_args[0]
1565 else:
1566 if self.GetBranch() is None:
1567 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1568
1569 # Default to diffing against common ancestor of upstream branch
1570 base_branch = self.GetCommonAncestorWithUpstream()
1571 git_diff_args = [base_branch, 'HEAD']
1572
1573 # Make sure authenticated to codereview before running potentially expensive
1574 # hooks. It is a fast, best efforts check. Codereview still can reject the
1575 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001576 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001577
1578 # Apply watchlists on upload.
1579 change = self.GetChange(base_branch, None)
1580 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1581 files = [f.LocalPath() for f in change.AffectedFiles()]
1582 if not options.bypass_watchlists:
1583 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1584
1585 if not options.bypass_hooks:
1586 if options.reviewers or options.tbr_owners:
1587 # Set the reviewer list now so that presubmit checks can access it.
1588 change_description = ChangeDescription(change.FullDescriptionText())
1589 change_description.update_reviewers(options.reviewers,
1590 options.tbr_owners,
1591 change)
1592 change.SetDescriptionText(change_description.description)
1593 hook_results = self.RunHook(committing=False,
1594 may_prompt=not options.force,
1595 verbose=options.verbose,
1596 change=change)
1597 if not hook_results.should_continue():
1598 return 1
1599 if not options.reviewers and hook_results.reviewers:
1600 options.reviewers = hook_results.reviewers.split(',')
1601
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001602 # TODO(tandrii): Checking local patchset against remote patchset is only
1603 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1604 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001605 latest_patchset = self.GetMostRecentPatchset()
1606 local_patchset = self.GetPatchset()
1607 if (latest_patchset and local_patchset and
1608 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001609 print('The last upload made from this repository was patchset #%d but '
1610 'the most recent patchset on the server is #%d.'
1611 % (local_patchset, latest_patchset))
1612 print('Uploading will still work, but if you\'ve uploaded to this '
1613 'issue from another machine or branch the patch you\'re '
1614 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001615 ask_for_data('About to upload; enter to confirm.')
1616
1617 print_stats(options.similarity, options.find_copies, git_diff_args)
1618 ret = self.CMDUploadChange(options, git_diff_args, change)
1619 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001620 if options.use_commit_queue:
1621 self.SetCQState(_CQState.COMMIT)
1622 elif options.cq_dry_run:
1623 self.SetCQState(_CQState.DRY_RUN)
1624
tandrii5d48c322016-08-18 16:19:37 -07001625 _git_set_branch_config_value('last-upload-hash',
1626 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 # Run post upload hooks, if specified.
1628 if settings.GetRunPostUploadHook():
1629 presubmit_support.DoPostUploadExecuter(
1630 change,
1631 self,
1632 settings.GetRoot(),
1633 options.verbose,
1634 sys.stdout)
1635
1636 # Upload all dependencies if specified.
1637 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001638 print()
1639 print('--dependencies has been specified.')
1640 print('All dependent local branches will be re-uploaded.')
1641 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001642 # Remove the dependencies flag from args so that we do not end up in a
1643 # loop.
1644 orig_args.remove('--dependencies')
1645 ret = upload_branch_deps(self, orig_args)
1646 return ret
1647
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001648 def SetCQState(self, new_state):
1649 """Update the CQ state for latest patchset.
1650
1651 Issue must have been already uploaded and known.
1652 """
1653 assert new_state in _CQState.ALL_STATES
1654 assert self.GetIssue()
1655 return self._codereview_impl.SetCQState(new_state)
1656
qyearsley1fdfcb62016-10-24 13:22:03 -07001657 def TriggerDryRun(self):
1658 """Triggers a dry run and prints a warning on failure."""
1659 # TODO(qyearsley): Either re-use this method in CMDset_commit
1660 # and CMDupload, or change CMDtry to trigger dry runs with
1661 # just SetCQState, and catch keyboard interrupt and other
1662 # errors in that method.
1663 try:
1664 self.SetCQState(_CQState.DRY_RUN)
1665 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1666 return 0
1667 except KeyboardInterrupt:
1668 raise
1669 except:
1670 print('WARNING: failed to trigger CQ Dry Run.\n'
1671 'Either:\n'
1672 ' * your project has no CQ\n'
1673 ' * you don\'t have permission to trigger Dry Run\n'
1674 ' * bug in this code (see stack trace below).\n'
1675 'Consider specifying which bots to trigger manually '
1676 'or asking your project owners for permissions '
1677 'or contacting Chrome Infrastructure team at '
1678 'https://www.chromium.org/infra\n\n')
1679 # Still raise exception so that stack trace is printed.
1680 raise
1681
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682 # Forward methods to codereview specific implementation.
1683
1684 def CloseIssue(self):
1685 return self._codereview_impl.CloseIssue()
1686
1687 def GetStatus(self):
1688 return self._codereview_impl.GetStatus()
1689
1690 def GetCodereviewServer(self):
1691 return self._codereview_impl.GetCodereviewServer()
1692
tandriide281ae2016-10-12 06:02:30 -07001693 def GetIssueOwner(self):
1694 """Get owner from codereview, which may differ from this checkout."""
1695 return self._codereview_impl.GetIssueOwner()
1696
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001697 def GetApprovingReviewers(self):
1698 return self._codereview_impl.GetApprovingReviewers()
1699
1700 def GetMostRecentPatchset(self):
1701 return self._codereview_impl.GetMostRecentPatchset()
1702
tandriide281ae2016-10-12 06:02:30 -07001703 def CannotTriggerTryJobReason(self):
1704 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1705 return self._codereview_impl.CannotTriggerTryJobReason()
1706
tandrii8c5a3532016-11-04 07:52:02 -07001707 def GetTryjobProperties(self, patchset=None):
1708 """Returns dictionary of properties to launch tryjob."""
1709 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1710
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001711 def __getattr__(self, attr):
1712 # This is because lots of untested code accesses Rietveld-specific stuff
1713 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001714 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001715 # Note that child method defines __getattr__ as well, and forwards it here,
1716 # because _RietveldChangelistImpl is not cleaned up yet, and given
1717 # deprecation of Rietveld, it should probably be just removed.
1718 # Until that time, avoid infinite recursion by bypassing __getattr__
1719 # of implementation class.
1720 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721
1722
1723class _ChangelistCodereviewBase(object):
1724 """Abstract base class encapsulating codereview specifics of a changelist."""
1725 def __init__(self, changelist):
1726 self._changelist = changelist # instance of Changelist
1727
1728 def __getattr__(self, attr):
1729 # Forward methods to changelist.
1730 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1731 # _RietveldChangelistImpl to avoid this hack?
1732 return getattr(self._changelist, attr)
1733
1734 def GetStatus(self):
1735 """Apply a rough heuristic to give a simple summary of an issue's review
1736 or CQ status, assuming adherence to a common workflow.
1737
1738 Returns None if no issue for this branch, or specific string keywords.
1739 """
1740 raise NotImplementedError()
1741
1742 def GetCodereviewServer(self):
1743 """Returns server URL without end slash, like "https://codereview.com"."""
1744 raise NotImplementedError()
1745
1746 def FetchDescription(self):
1747 """Fetches and returns description from the codereview server."""
1748 raise NotImplementedError()
1749
tandrii5d48c322016-08-18 16:19:37 -07001750 @classmethod
1751 def IssueConfigKey(cls):
1752 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 raise NotImplementedError()
1754
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001755 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001756 def PatchsetConfigKey(cls):
1757 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 raise NotImplementedError()
1759
tandrii5d48c322016-08-18 16:19:37 -07001760 @classmethod
1761 def CodereviewServerConfigKey(cls):
1762 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763 raise NotImplementedError()
1764
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001765 def _PostUnsetIssueProperties(self):
1766 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001767 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001768
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769 def GetRieveldObjForPresubmit(self):
1770 # This is an unfortunate Rietveld-embeddedness in presubmit.
1771 # For non-Rietveld codereviews, this probably should return a dummy object.
1772 raise NotImplementedError()
1773
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001774 def GetGerritObjForPresubmit(self):
1775 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1776 return None
1777
dsansomee2d6fd92016-09-08 00:10:47 -07001778 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001779 """Update the description on codereview site."""
1780 raise NotImplementedError()
1781
1782 def CloseIssue(self):
1783 """Closes the issue."""
1784 raise NotImplementedError()
1785
1786 def GetApprovingReviewers(self):
1787 """Returns a list of reviewers approving the change.
1788
1789 Note: not necessarily committers.
1790 """
1791 raise NotImplementedError()
1792
1793 def GetMostRecentPatchset(self):
1794 """Returns the most recent patchset number from the codereview site."""
1795 raise NotImplementedError()
1796
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001797 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1798 directory):
1799 """Fetches and applies the issue.
1800
1801 Arguments:
1802 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1803 reject: if True, reject the failed patch instead of switching to 3-way
1804 merge. Rietveld only.
1805 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1806 only.
1807 directory: switch to directory before applying the patch. Rietveld only.
1808 """
1809 raise NotImplementedError()
1810
1811 @staticmethod
1812 def ParseIssueURL(parsed_url):
1813 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1814 failed."""
1815 raise NotImplementedError()
1816
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001817 def EnsureAuthenticated(self, force):
1818 """Best effort check that user is authenticated with codereview server.
1819
1820 Arguments:
1821 force: whether to skip confirmation questions.
1822 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001823 raise NotImplementedError()
1824
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001825 def CMDUploadChange(self, options, args, change):
1826 """Uploads a change to codereview."""
1827 raise NotImplementedError()
1828
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001829 def SetCQState(self, new_state):
1830 """Update the CQ state for latest patchset.
1831
1832 Issue must have been already uploaded and known.
1833 """
1834 raise NotImplementedError()
1835
tandriie113dfd2016-10-11 10:20:12 -07001836 def CannotTriggerTryJobReason(self):
1837 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1838 raise NotImplementedError()
1839
tandriide281ae2016-10-12 06:02:30 -07001840 def GetIssueOwner(self):
1841 raise NotImplementedError()
1842
tandrii8c5a3532016-11-04 07:52:02 -07001843 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001844 raise NotImplementedError()
1845
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001846
1847class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1848 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1849 super(_RietveldChangelistImpl, self).__init__(changelist)
1850 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001851 if not rietveld_server:
1852 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001853
1854 self._rietveld_server = rietveld_server
1855 self._auth_config = auth_config
1856 self._props = None
1857 self._rpc_server = None
1858
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001859 def GetCodereviewServer(self):
1860 if not self._rietveld_server:
1861 # If we're on a branch then get the server potentially associated
1862 # with that branch.
1863 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001864 self._rietveld_server = gclient_utils.UpgradeToHttps(
1865 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001866 if not self._rietveld_server:
1867 self._rietveld_server = settings.GetDefaultServerUrl()
1868 return self._rietveld_server
1869
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001870 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001871 """Best effort check that user is authenticated with Rietveld server."""
1872 if self._auth_config.use_oauth2:
1873 authenticator = auth.get_authenticator_for_host(
1874 self.GetCodereviewServer(), self._auth_config)
1875 if not authenticator.has_cached_credentials():
1876 raise auth.LoginRequiredError(self.GetCodereviewServer())
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def FetchDescription(self):
1879 issue = self.GetIssue()
1880 assert issue
1881 try:
1882 return self.RpcServer().get_description(issue).strip()
1883 except urllib2.HTTPError as e:
1884 if e.code == 404:
1885 DieWithError(
1886 ('\nWhile fetching the description for issue %d, received a '
1887 '404 (not found)\n'
1888 'error. It is likely that you deleted this '
1889 'issue on the server. If this is the\n'
1890 'case, please run\n\n'
1891 ' git cl issue 0\n\n'
1892 'to clear the association with the deleted issue. Then run '
1893 'this command again.') % issue)
1894 else:
1895 DieWithError(
1896 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1897 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001898 print('Warning: Failed to retrieve CL description due to network '
1899 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001900 return ''
1901
1902 def GetMostRecentPatchset(self):
1903 return self.GetIssueProperties()['patchsets'][-1]
1904
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 def GetIssueProperties(self):
1906 if self._props is None:
1907 issue = self.GetIssue()
1908 if not issue:
1909 self._props = {}
1910 else:
1911 self._props = self.RpcServer().get_issue_properties(issue, True)
1912 return self._props
1913
tandriie113dfd2016-10-11 10:20:12 -07001914 def CannotTriggerTryJobReason(self):
1915 props = self.GetIssueProperties()
1916 if not props:
1917 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1918 if props.get('closed'):
1919 return 'CL %s is closed' % self.GetIssue()
1920 if props.get('private'):
1921 return 'CL %s is private' % self.GetIssue()
1922 return None
1923
tandrii8c5a3532016-11-04 07:52:02 -07001924 def GetTryjobProperties(self, patchset=None):
1925 """Returns dictionary of properties to launch tryjob."""
1926 project = (self.GetIssueProperties() or {}).get('project')
1927 return {
1928 'issue': self.GetIssue(),
1929 'patch_project': project,
1930 'patch_storage': 'rietveld',
1931 'patchset': patchset or self.GetPatchset(),
1932 'rietveld': self.GetCodereviewServer(),
1933 }
1934
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001935 def GetApprovingReviewers(self):
1936 return get_approving_reviewers(self.GetIssueProperties())
1937
tandriide281ae2016-10-12 06:02:30 -07001938 def GetIssueOwner(self):
1939 return (self.GetIssueProperties() or {}).get('owner_email')
1940
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941 def AddComment(self, message):
1942 return self.RpcServer().add_comment(self.GetIssue(), message)
1943
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001944 def GetStatus(self):
1945 """Apply a rough heuristic to give a simple summary of an issue's review
1946 or CQ status, assuming adherence to a common workflow.
1947
1948 Returns None if no issue for this branch, or one of the following keywords:
1949 * 'error' - error from review tool (including deleted issues)
1950 * 'unsent' - not sent for review
1951 * 'waiting' - waiting for review
1952 * 'reply' - waiting for owner to reply to review
1953 * 'lgtm' - LGTM from at least one approved reviewer
1954 * 'commit' - in the commit queue
1955 * 'closed' - closed
1956 """
1957 if not self.GetIssue():
1958 return None
1959
1960 try:
1961 props = self.GetIssueProperties()
1962 except urllib2.HTTPError:
1963 return 'error'
1964
1965 if props.get('closed'):
1966 # Issue is closed.
1967 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001968 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001969 # Issue is in the commit queue.
1970 return 'commit'
1971
1972 try:
1973 reviewers = self.GetApprovingReviewers()
1974 except urllib2.HTTPError:
1975 return 'error'
1976
1977 if reviewers:
1978 # Was LGTM'ed.
1979 return 'lgtm'
1980
1981 messages = props.get('messages') or []
1982
tandrii9d2c7a32016-06-22 03:42:45 -07001983 # Skip CQ messages that don't require owner's action.
1984 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1985 if 'Dry run:' in messages[-1]['text']:
1986 messages.pop()
1987 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1988 # This message always follows prior messages from CQ,
1989 # so skip this too.
1990 messages.pop()
1991 else:
1992 # This is probably a CQ messages warranting user attention.
1993 break
1994
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001995 if not messages:
1996 # No message was sent.
1997 return 'unsent'
1998 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001999 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002000 return 'reply'
2001 return 'waiting'
2002
dsansomee2d6fd92016-09-08 00:10:47 -07002003 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002004 return self.RpcServer().update_description(
2005 self.GetIssue(), self.description)
2006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002008 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002010 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002011 return self.SetFlags({flag: value})
2012
2013 def SetFlags(self, flags):
2014 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002015 """
phajdan.jr68598232016-08-10 03:28:28 -07002016 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002017 try:
tandrii4b233bd2016-07-06 03:50:29 -07002018 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002019 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002020 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002021 if e.code == 404:
2022 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2023 if e.code == 403:
2024 DieWithError(
2025 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002026 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002027 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002029 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030 """Returns an upload.RpcServer() to access this review's rietveld instance.
2031 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002032 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002033 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002034 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002035 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002036 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002037
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002038 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002039 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002040 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041
tandrii5d48c322016-08-18 16:19:37 -07002042 @classmethod
2043 def PatchsetConfigKey(cls):
2044 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045
tandrii5d48c322016-08-18 16:19:37 -07002046 @classmethod
2047 def CodereviewServerConfigKey(cls):
2048 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002049
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002050 def GetRieveldObjForPresubmit(self):
2051 return self.RpcServer()
2052
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002053 def SetCQState(self, new_state):
2054 props = self.GetIssueProperties()
2055 if props.get('private'):
2056 DieWithError('Cannot set-commit on private issue')
2057
2058 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002059 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002060 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002061 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002062 else:
tandrii4b233bd2016-07-06 03:50:29 -07002063 assert new_state == _CQState.DRY_RUN
2064 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002065
2066
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002067 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2068 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002069 # PatchIssue should never be called with a dirty tree. It is up to the
2070 # caller to check this, but just in case we assert here since the
2071 # consequences of the caller not checking this could be dire.
2072 assert(not git_common.is_dirty_git_tree('apply'))
2073 assert(parsed_issue_arg.valid)
2074 self._changelist.issue = parsed_issue_arg.issue
2075 if parsed_issue_arg.hostname:
2076 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2077
skobes6468b902016-10-24 08:45:10 -07002078 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2079 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2080 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002081 try:
skobes6468b902016-10-24 08:45:10 -07002082 scm_obj.apply_patch(patchset_object)
2083 except Exception as e:
2084 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002085 return 1
2086
2087 # If we had an issue, commit the current state and register the issue.
2088 if not nocommit:
2089 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2090 'patch from issue %(i)s at patchset '
2091 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2092 % {'i': self.GetIssue(), 'p': patchset})])
2093 self.SetIssue(self.GetIssue())
2094 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002095 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002096 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002097 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002098 return 0
2099
2100 @staticmethod
2101 def ParseIssueURL(parsed_url):
2102 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2103 return None
wychen3c1c1722016-08-04 11:46:36 -07002104 # Rietveld patch: https://domain/<number>/#ps<patchset>
2105 match = re.match(r'/(\d+)/$', parsed_url.path)
2106 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2107 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002108 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002109 issue=int(match.group(1)),
2110 patchset=int(match2.group(1)),
2111 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002112 # Typical url: https://domain/<issue_number>[/[other]]
2113 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2114 if match:
skobes6468b902016-10-24 08:45:10 -07002115 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002116 issue=int(match.group(1)),
2117 hostname=parsed_url.netloc)
2118 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2119 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2120 if match:
skobes6468b902016-10-24 08:45:10 -07002121 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002122 issue=int(match.group(1)),
2123 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002124 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002125 return None
2126
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002127 def CMDUploadChange(self, options, args, change):
2128 """Upload the patch to Rietveld."""
2129 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2130 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002131 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2132 if options.emulate_svn_auto_props:
2133 upload_args.append('--emulate_svn_auto_props')
2134
2135 change_desc = None
2136
2137 if options.email is not None:
2138 upload_args.extend(['--email', options.email])
2139
2140 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002141 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002142 upload_args.extend(['--title', options.title])
2143 if options.message:
2144 upload_args.extend(['--message', options.message])
2145 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002146 print('This branch is associated with issue %s. '
2147 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002148 else:
nodirca166002016-06-27 10:59:51 -07002149 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002150 upload_args.extend(['--title', options.title])
2151 message = (options.title or options.message or
2152 CreateDescriptionFromLog(args))
2153 change_desc = ChangeDescription(message)
2154 if options.reviewers or options.tbr_owners:
2155 change_desc.update_reviewers(options.reviewers,
2156 options.tbr_owners,
2157 change)
2158 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002159 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002160
2161 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002162 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002163 return 1
2164
2165 upload_args.extend(['--message', change_desc.description])
2166 if change_desc.get_reviewers():
2167 upload_args.append('--reviewers=%s' % ','.join(
2168 change_desc.get_reviewers()))
2169 if options.send_mail:
2170 if not change_desc.get_reviewers():
2171 DieWithError("Must specify reviewers to send email.")
2172 upload_args.append('--send_mail')
2173
2174 # We check this before applying rietveld.private assuming that in
2175 # rietveld.cc only addresses which we can send private CLs to are listed
2176 # if rietveld.private is set, and so we should ignore rietveld.cc only
2177 # when --private is specified explicitly on the command line.
2178 if options.private:
2179 logging.warn('rietveld.cc is ignored since private flag is specified. '
2180 'You need to review and add them manually if necessary.')
2181 cc = self.GetCCListWithoutDefault()
2182 else:
2183 cc = self.GetCCList()
2184 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002185 if change_desc.get_cced():
2186 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002187 if cc:
2188 upload_args.extend(['--cc', cc])
2189
2190 if options.private or settings.GetDefaultPrivateFlag() == "True":
2191 upload_args.append('--private')
2192
2193 upload_args.extend(['--git_similarity', str(options.similarity)])
2194 if not options.find_copies:
2195 upload_args.extend(['--git_no_find_copies'])
2196
2197 # Include the upstream repo's URL in the change -- this is useful for
2198 # projects that have their source spread across multiple repos.
2199 remote_url = self.GetGitBaseUrlFromConfig()
2200 if not remote_url:
2201 if settings.GetIsGitSvn():
2202 remote_url = self.GetGitSvnRemoteUrl()
2203 else:
2204 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2205 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2206 self.GetUpstreamBranch().split('/')[-1])
2207 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002208 remote, remote_branch = self.GetRemoteBranch()
2209 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2210 settings.GetPendingRefPrefix())
2211 if target_ref:
2212 upload_args.extend(['--target_ref', target_ref])
2213
2214 # Look for dependent patchsets. See crbug.com/480453 for more details.
2215 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2216 upstream_branch = ShortBranchName(upstream_branch)
2217 if remote is '.':
2218 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002219 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002221 print()
2222 print('Skipping dependency patchset upload because git config '
2223 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2224 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002225 else:
2226 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002227 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 auth_config=auth_config)
2229 branch_cl_issue_url = branch_cl.GetIssueURL()
2230 branch_cl_issue = branch_cl.GetIssue()
2231 branch_cl_patchset = branch_cl.GetPatchset()
2232 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2233 upload_args.extend(
2234 ['--depends_on_patchset', '%s:%s' % (
2235 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002236 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002237 '\n'
2238 'The current branch (%s) is tracking a local branch (%s) with '
2239 'an associated CL.\n'
2240 'Adding %s/#ps%s as a dependency patchset.\n'
2241 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2242 branch_cl_patchset))
2243
2244 project = settings.GetProject()
2245 if project:
2246 upload_args.extend(['--project', project])
2247
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248 try:
2249 upload_args = ['upload'] + upload_args + args
2250 logging.info('upload.RealMain(%s)', upload_args)
2251 issue, patchset = upload.RealMain(upload_args)
2252 issue = int(issue)
2253 patchset = int(patchset)
2254 except KeyboardInterrupt:
2255 sys.exit(1)
2256 except:
2257 # If we got an exception after the user typed a description for their
2258 # change, back up the description before re-raising.
2259 if change_desc:
2260 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2261 print('\nGot exception while uploading -- saving description to %s\n' %
2262 backup_path)
2263 backup_file = open(backup_path, 'w')
2264 backup_file.write(change_desc.description)
2265 backup_file.close()
2266 raise
2267
2268 if not self.GetIssue():
2269 self.SetIssue(issue)
2270 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002271 return 0
2272
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002273
2274class _GerritChangelistImpl(_ChangelistCodereviewBase):
2275 def __init__(self, changelist, auth_config=None):
2276 # auth_config is Rietveld thing, kept here to preserve interface only.
2277 super(_GerritChangelistImpl, self).__init__(changelist)
2278 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002279 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002280 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002281 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002282
2283 def _GetGerritHost(self):
2284 # Lazy load of configs.
2285 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002286 if self._gerrit_host and '.' not in self._gerrit_host:
2287 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2288 # This happens for internal stuff http://crbug.com/614312.
2289 parsed = urlparse.urlparse(self.GetRemoteUrl())
2290 if parsed.scheme == 'sso':
2291 print('WARNING: using non https URLs for remote is likely broken\n'
2292 ' Your current remote is: %s' % self.GetRemoteUrl())
2293 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2294 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002295 return self._gerrit_host
2296
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002297 def _GetGitHost(self):
2298 """Returns git host to be used when uploading change to Gerrit."""
2299 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2300
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002301 def GetCodereviewServer(self):
2302 if not self._gerrit_server:
2303 # If we're on a branch then get the server potentially associated
2304 # with that branch.
2305 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002306 self._gerrit_server = self._GitGetBranchConfigValue(
2307 self.CodereviewServerConfigKey())
2308 if self._gerrit_server:
2309 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002310 if not self._gerrit_server:
2311 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2312 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002313 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002314 parts[0] = parts[0] + '-review'
2315 self._gerrit_host = '.'.join(parts)
2316 self._gerrit_server = 'https://%s' % self._gerrit_host
2317 return self._gerrit_server
2318
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002319 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002320 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002321 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002322
tandrii5d48c322016-08-18 16:19:37 -07002323 @classmethod
2324 def PatchsetConfigKey(cls):
2325 return 'gerritpatchset'
2326
2327 @classmethod
2328 def CodereviewServerConfigKey(cls):
2329 return 'gerritserver'
2330
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002331 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002332 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002333 if settings.GetGerritSkipEnsureAuthenticated():
2334 # For projects with unusual authentication schemes.
2335 # See http://crbug.com/603378.
2336 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002337 # Lazy-loader to identify Gerrit and Git hosts.
2338 if gerrit_util.GceAuthenticator.is_gce():
2339 return
2340 self.GetCodereviewServer()
2341 git_host = self._GetGitHost()
2342 assert self._gerrit_server and self._gerrit_host
2343 cookie_auth = gerrit_util.CookiesAuthenticator()
2344
2345 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2346 git_auth = cookie_auth.get_auth_header(git_host)
2347 if gerrit_auth and git_auth:
2348 if gerrit_auth == git_auth:
2349 return
2350 print((
2351 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2352 ' Check your %s or %s file for credentials of hosts:\n'
2353 ' %s\n'
2354 ' %s\n'
2355 ' %s') %
2356 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2357 git_host, self._gerrit_host,
2358 cookie_auth.get_new_password_message(git_host)))
2359 if not force:
2360 ask_for_data('If you know what you are doing, press Enter to continue, '
2361 'Ctrl+C to abort.')
2362 return
2363 else:
2364 missing = (
2365 [] if gerrit_auth else [self._gerrit_host] +
2366 [] if git_auth else [git_host])
2367 DieWithError('Credentials for the following hosts are required:\n'
2368 ' %s\n'
2369 'These are read from %s (or legacy %s)\n'
2370 '%s' % (
2371 '\n '.join(missing),
2372 cookie_auth.get_gitcookies_path(),
2373 cookie_auth.get_netrc_path(),
2374 cookie_auth.get_new_password_message(git_host)))
2375
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002376 def _PostUnsetIssueProperties(self):
2377 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002378 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002379
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002380 def GetRieveldObjForPresubmit(self):
2381 class ThisIsNotRietveldIssue(object):
2382 def __nonzero__(self):
2383 # This is a hack to make presubmit_support think that rietveld is not
2384 # defined, yet still ensure that calls directly result in a decent
2385 # exception message below.
2386 return False
2387
2388 def __getattr__(self, attr):
2389 print(
2390 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2391 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2392 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2393 'or use Rietveld for codereview.\n'
2394 'See also http://crbug.com/579160.' % attr)
2395 raise NotImplementedError()
2396 return ThisIsNotRietveldIssue()
2397
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002398 def GetGerritObjForPresubmit(self):
2399 return presubmit_support.GerritAccessor(self._GetGerritHost())
2400
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002401 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002402 """Apply a rough heuristic to give a simple summary of an issue's review
2403 or CQ status, assuming adherence to a common workflow.
2404
2405 Returns None if no issue for this branch, or one of the following keywords:
2406 * 'error' - error from review tool (including deleted issues)
2407 * 'unsent' - no reviewers added
2408 * 'waiting' - waiting for review
2409 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002410 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2411 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002412 * 'commit' - in the commit queue
2413 * 'closed' - abandoned
2414 """
2415 if not self.GetIssue():
2416 return None
2417
2418 try:
2419 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002420 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002421 return 'error'
2422
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002423 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002424 return 'closed'
2425
2426 cq_label = data['labels'].get('Commit-Queue', {})
2427 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002428 votes = cq_label.get('all', [])
2429 highest_vote = 0
2430 for v in votes:
2431 highest_vote = max(highest_vote, v.get('value', 0))
2432 vote_value = str(highest_vote)
2433 if vote_value != '0':
2434 # Add a '+' if the value is not 0 to match the values in the label.
2435 # The cq_label does not have negatives.
2436 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002437 vote_text = cq_label.get('values', {}).get(vote_value, '')
2438 if vote_text.lower() == 'commit':
2439 return 'commit'
2440
2441 lgtm_label = data['labels'].get('Code-Review', {})
2442 if lgtm_label:
2443 if 'rejected' in lgtm_label:
2444 return 'not lgtm'
2445 if 'approved' in lgtm_label:
2446 return 'lgtm'
2447
2448 if not data.get('reviewers', {}).get('REVIEWER', []):
2449 return 'unsent'
2450
2451 messages = data.get('messages', [])
2452 if messages:
2453 owner = data['owner'].get('_account_id')
2454 last_message_author = messages[-1].get('author', {}).get('_account_id')
2455 if owner != last_message_author:
2456 # Some reply from non-owner.
2457 return 'reply'
2458
2459 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002460
2461 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002462 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002463 return data['revisions'][data['current_revision']]['_number']
2464
2465 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002466 data = self._GetChangeDetail(['CURRENT_REVISION'])
2467 current_rev = data['current_revision']
2468 url = data['revisions'][current_rev]['fetch']['http']['url']
2469 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002470
dsansomee2d6fd92016-09-08 00:10:47 -07002471 def UpdateDescriptionRemote(self, description, force=False):
2472 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2473 if not force:
2474 ask_for_data(
2475 'The description cannot be modified while the issue has a pending '
2476 'unpublished edit. Either publish the edit in the Gerrit web UI '
2477 'or delete it.\n\n'
2478 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2479
2480 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2481 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002482 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002483 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002484
2485 def CloseIssue(self):
2486 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2487
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002488 def GetApprovingReviewers(self):
2489 """Returns a list of reviewers approving the change.
2490
2491 Note: not necessarily committers.
2492 """
2493 raise NotImplementedError()
2494
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002495 def SubmitIssue(self, wait_for_merge=True):
2496 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2497 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002498
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002499 def _GetChangeDetail(self, options=None, issue=None):
2500 options = options or []
2501 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002502 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002503 try:
2504 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2505 options, ignore_404=False)
2506 except gerrit_util.GerritError as e:
2507 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002508 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002509 raise
tandriic2405f52016-10-10 08:13:15 -07002510 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002511
agable32978d92016-11-01 12:55:02 -07002512 def _GetChangeCommit(self, issue=None):
2513 issue = issue or self.GetIssue()
2514 assert issue, 'issue is required to query Gerrit'
2515 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2516 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002517 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002518 return data
2519
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002520 def CMDLand(self, force, bypass_hooks, verbose):
2521 if git_common.is_dirty_git_tree('land'):
2522 return 1
tandriid60367b2016-06-22 05:25:12 -07002523 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2524 if u'Commit-Queue' in detail.get('labels', {}):
2525 if not force:
2526 ask_for_data('\nIt seems this repository has a Commit Queue, '
2527 'which can test and land changes for you. '
2528 'Are you sure you wish to bypass it?\n'
2529 'Press Enter to continue, Ctrl+C to abort.')
2530
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002531 differs = True
tandriic4344b52016-08-29 06:04:54 -07002532 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002533 # Note: git diff outputs nothing if there is no diff.
2534 if not last_upload or RunGit(['diff', last_upload]).strip():
2535 print('WARNING: some changes from local branch haven\'t been uploaded')
2536 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002537 if detail['current_revision'] == last_upload:
2538 differs = False
2539 else:
2540 print('WARNING: local branch contents differ from latest uploaded '
2541 'patchset')
2542 if differs:
2543 if not force:
2544 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002545 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2546 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002547 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2548 elif not bypass_hooks:
2549 hook_results = self.RunHook(
2550 committing=True,
2551 may_prompt=not force,
2552 verbose=verbose,
2553 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2554 if not hook_results.should_continue():
2555 return 1
2556
2557 self.SubmitIssue(wait_for_merge=True)
2558 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002559 links = self._GetChangeCommit().get('web_links', [])
2560 for link in links:
2561 if link.get('name') == 'gerrit' and link.get('url'):
2562 print('Landed as %s' % link.get('url'))
2563 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002564 return 0
2565
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002566 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2567 directory):
2568 assert not reject
2569 assert not nocommit
2570 assert not directory
2571 assert parsed_issue_arg.valid
2572
2573 self._changelist.issue = parsed_issue_arg.issue
2574
2575 if parsed_issue_arg.hostname:
2576 self._gerrit_host = parsed_issue_arg.hostname
2577 self._gerrit_server = 'https://%s' % self._gerrit_host
2578
tandriic2405f52016-10-10 08:13:15 -07002579 try:
2580 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002581 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002582 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002583
2584 if not parsed_issue_arg.patchset:
2585 # Use current revision by default.
2586 revision_info = detail['revisions'][detail['current_revision']]
2587 patchset = int(revision_info['_number'])
2588 else:
2589 patchset = parsed_issue_arg.patchset
2590 for revision_info in detail['revisions'].itervalues():
2591 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2592 break
2593 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002594 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002595 (parsed_issue_arg.patchset, self.GetIssue()))
2596
2597 fetch_info = revision_info['fetch']['http']
2598 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2599 RunGit(['cherry-pick', 'FETCH_HEAD'])
2600 self.SetIssue(self.GetIssue())
2601 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002602 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002603 (self.GetIssue(), self.GetPatchset()))
2604 return 0
2605
2606 @staticmethod
2607 def ParseIssueURL(parsed_url):
2608 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2609 return None
2610 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2611 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2612 # Short urls like https://domain/<issue_number> can be used, but don't allow
2613 # specifying the patchset (you'd 404), but we allow that here.
2614 if parsed_url.path == '/':
2615 part = parsed_url.fragment
2616 else:
2617 part = parsed_url.path
2618 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2619 if match:
2620 return _ParsedIssueNumberArgument(
2621 issue=int(match.group(2)),
2622 patchset=int(match.group(4)) if match.group(4) else None,
2623 hostname=parsed_url.netloc)
2624 return None
2625
tandrii16e0b4e2016-06-07 10:34:28 -07002626 def _GerritCommitMsgHookCheck(self, offer_removal):
2627 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2628 if not os.path.exists(hook):
2629 return
2630 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2631 # custom developer made one.
2632 data = gclient_utils.FileRead(hook)
2633 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2634 return
2635 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002636 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002637 'and may interfere with it in subtle ways.\n'
2638 'We recommend you remove the commit-msg hook.')
2639 if offer_removal:
2640 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2641 if reply.lower().startswith('y'):
2642 gclient_utils.rm_file_or_tree(hook)
2643 print('Gerrit commit-msg hook removed.')
2644 else:
2645 print('OK, will keep Gerrit commit-msg hook in place.')
2646
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002647 def CMDUploadChange(self, options, args, change):
2648 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002649 if options.squash and options.no_squash:
2650 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002651
2652 if not options.squash and not options.no_squash:
2653 # Load default for user, repo, squash=true, in this order.
2654 options.squash = settings.GetSquashGerritUploads()
2655 elif options.no_squash:
2656 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002657
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002658 # We assume the remote called "origin" is the one we want.
2659 # It is probably not worthwhile to support different workflows.
2660 gerrit_remote = 'origin'
2661
2662 remote, remote_branch = self.GetRemoteBranch()
2663 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2664 pending_prefix='')
2665
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002666 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002667 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 if self.GetIssue():
2669 # Try to get the message from a previous upload.
2670 message = self.GetDescription()
2671 if not message:
2672 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002673 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002674 '%s' % (self.GetIssue(), self.GetIssueURL()))
2675 change_id = self._GetChangeDetail()['change_id']
2676 while True:
2677 footer_change_ids = git_footers.get_footer_change_id(message)
2678 if footer_change_ids == [change_id]:
2679 break
2680 if not footer_change_ids:
2681 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002682 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683 continue
2684 # There is already a valid footer but with different or several ids.
2685 # Doing this automatically is non-trivial as we don't want to lose
2686 # existing other footers, yet we want to append just 1 desired
2687 # Change-Id. Thus, just create a new footer, but let user verify the
2688 # new description.
2689 message = '%s\n\nChange-Id: %s' % (message, change_id)
2690 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002691 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002692 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002693 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002694 'Please, check the proposed correction to the description, '
2695 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2696 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2697 change_id))
2698 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2699 if not options.force:
2700 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002701 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002702 message = change_desc.description
2703 if not message:
2704 DieWithError("Description is empty. Aborting...")
2705 # Continue the while loop.
2706 # Sanity check of this code - we should end up with proper message
2707 # footer.
2708 assert [change_id] == git_footers.get_footer_change_id(message)
2709 change_desc = ChangeDescription(message)
2710 else:
2711 change_desc = ChangeDescription(
2712 options.message or CreateDescriptionFromLog(args))
2713 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002714 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002715 if not change_desc.description:
2716 DieWithError("Description is empty. Aborting...")
2717 message = change_desc.description
2718 change_ids = git_footers.get_footer_change_id(message)
2719 if len(change_ids) > 1:
2720 DieWithError('too many Change-Id footers, at most 1 allowed.')
2721 if not change_ids:
2722 # Generate the Change-Id automatically.
2723 message = git_footers.add_footer_change_id(
2724 message, GenerateGerritChangeId(message))
2725 change_desc.set_description(message)
2726 change_ids = git_footers.get_footer_change_id(message)
2727 assert len(change_ids) == 1
2728 change_id = change_ids[0]
2729
2730 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2731 if remote is '.':
2732 # If our upstream branch is local, we base our squashed commit on its
2733 # squashed version.
2734 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2735 # Check the squashed hash of the parent.
2736 parent = RunGit(['config',
2737 'branch.%s.gerritsquashhash' % upstream_branch_name],
2738 error_ok=True).strip()
2739 # Verify that the upstream branch has been uploaded too, otherwise
2740 # Gerrit will create additional CLs when uploading.
2741 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2742 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002743 DieWithError(
2744 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002745 'Note: maybe you\'ve uploaded it with --no-squash. '
2746 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002747 ' git cl upload --squash\n' % upstream_branch_name)
2748 else:
2749 parent = self.GetCommonAncestorWithUpstream()
2750
2751 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2752 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2753 '-m', message]).strip()
2754 else:
2755 change_desc = ChangeDescription(
2756 options.message or CreateDescriptionFromLog(args))
2757 if not change_desc.description:
2758 DieWithError("Description is empty. Aborting...")
2759
2760 if not git_footers.get_footer_change_id(change_desc.description):
2761 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002762 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2763 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002764 ref_to_push = 'HEAD'
2765 parent = '%s/%s' % (gerrit_remote, branch)
2766 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2767
2768 assert change_desc
2769 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2770 ref_to_push)]).splitlines()
2771 if len(commits) > 1:
2772 print('WARNING: This will upload %d commits. Run the following command '
2773 'to see which commits will be uploaded: ' % len(commits))
2774 print('git log %s..%s' % (parent, ref_to_push))
2775 print('You can also use `git squash-branch` to squash these into a '
2776 'single commit.')
2777 ask_for_data('About to upload; enter to confirm.')
2778
2779 if options.reviewers or options.tbr_owners:
2780 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2781 change)
2782
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002783 # Extra options that can be specified at push time. Doc:
2784 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2785 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002786 if change_desc.get_reviewers(tbr_only=True):
2787 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2788 refspec_opts.append('l=Code-Review+1')
2789
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002790 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002791 if not re.match(r'^[\w ]+$', options.title):
2792 options.title = re.sub(r'[^\w ]', '', options.title)
2793 print('WARNING: Patchset title may only contain alphanumeric chars '
2794 'and spaces. Cleaned up title:\n%s' % options.title)
2795 if not options.force:
2796 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002797 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2798 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002799 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2800
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002801 if options.send_mail:
2802 if not change_desc.get_reviewers():
2803 DieWithError('Must specify reviewers to send email.')
2804 refspec_opts.append('notify=ALL')
2805 else:
2806 refspec_opts.append('notify=NONE')
2807
tandrii99a72f22016-08-17 14:33:24 -07002808 reviewers = change_desc.get_reviewers()
2809 if reviewers:
2810 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002811
agablec6787972016-09-09 16:13:34 -07002812 if options.private:
2813 refspec_opts.append('draft')
2814
rmistry9eadede2016-09-19 11:22:43 -07002815 if options.topic:
2816 # Documentation on Gerrit topics is here:
2817 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2818 refspec_opts.append('topic=%s' % options.topic)
2819
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002820 refspec_suffix = ''
2821 if refspec_opts:
2822 refspec_suffix = '%' + ','.join(refspec_opts)
2823 assert ' ' not in refspec_suffix, (
2824 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002825 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002826
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002827 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002828 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002829 print_stdout=True,
2830 # Flush after every line: useful for seeing progress when running as
2831 # recipe.
2832 filter_fn=lambda _: sys.stdout.flush())
2833
2834 if options.squash:
2835 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2836 change_numbers = [m.group(1)
2837 for m in map(regex.match, push_stdout.splitlines())
2838 if m]
2839 if len(change_numbers) != 1:
2840 DieWithError(
2841 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2842 'Change-Id: %s') % (len(change_numbers), change_id))
2843 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002844 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002845
2846 # Add cc's from the CC_LIST and --cc flag (if any).
2847 cc = self.GetCCList().split(',')
2848 if options.cc:
2849 cc.extend(options.cc)
2850 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002851 if change_desc.get_cced():
2852 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002853 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002854 gerrit_util.AddReviewers(
tandrii88189772016-09-29 04:29:57 -07002855 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002856 return 0
2857
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002858 def _AddChangeIdToCommitMessage(self, options, args):
2859 """Re-commits using the current message, assumes the commit hook is in
2860 place.
2861 """
2862 log_desc = options.message or CreateDescriptionFromLog(args)
2863 git_command = ['commit', '--amend', '-m', log_desc]
2864 RunGit(git_command)
2865 new_log_desc = CreateDescriptionFromLog(args)
2866 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002867 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002868 return new_log_desc
2869 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002870 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002871
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002872 def SetCQState(self, new_state):
2873 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002874 vote_map = {
2875 _CQState.NONE: 0,
2876 _CQState.DRY_RUN: 1,
2877 _CQState.COMMIT : 2,
2878 }
2879 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2880 labels={'Commit-Queue': vote_map[new_state]})
2881
tandriie113dfd2016-10-11 10:20:12 -07002882 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002883 try:
2884 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002885 except GerritChangeNotExists:
2886 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002887
2888 if data['status'] in ('ABANDONED', 'MERGED'):
2889 return 'CL %s is closed' % self.GetIssue()
2890
2891 def GetTryjobProperties(self, patchset=None):
2892 """Returns dictionary of properties to launch tryjob."""
2893 data = self._GetChangeDetail(['ALL_REVISIONS'])
2894 patchset = int(patchset or self.GetPatchset())
2895 assert patchset
2896 revision_data = None # Pylint wants it to be defined.
2897 for revision_data in data['revisions'].itervalues():
2898 if int(revision_data['_number']) == patchset:
2899 break
2900 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002901 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002902 (patchset, self.GetIssue()))
2903 return {
2904 'patch_issue': self.GetIssue(),
2905 'patch_set': patchset or self.GetPatchset(),
2906 'patch_project': data['project'],
2907 'patch_storage': 'gerrit',
2908 'patch_ref': revision_data['fetch']['http']['ref'],
2909 'patch_repository_url': revision_data['fetch']['http']['url'],
2910 'patch_gerrit_url': self.GetCodereviewServer(),
2911 }
tandriie113dfd2016-10-11 10:20:12 -07002912
tandriide281ae2016-10-12 06:02:30 -07002913 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002914 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002915
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002916
2917_CODEREVIEW_IMPLEMENTATIONS = {
2918 'rietveld': _RietveldChangelistImpl,
2919 'gerrit': _GerritChangelistImpl,
2920}
2921
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002922
iannuccie53c9352016-08-17 14:40:40 -07002923def _add_codereview_issue_select_options(parser, extra=""):
2924 _add_codereview_select_options(parser)
2925
2926 text = ('Operate on this issue number instead of the current branch\'s '
2927 'implicit issue.')
2928 if extra:
2929 text += ' '+extra
2930 parser.add_option('-i', '--issue', type=int, help=text)
2931
2932
2933def _process_codereview_issue_select_options(parser, options):
2934 _process_codereview_select_options(parser, options)
2935 if options.issue is not None and not options.forced_codereview:
2936 parser.error('--issue must be specified with either --rietveld or --gerrit')
2937
2938
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002939def _add_codereview_select_options(parser):
2940 """Appends --gerrit and --rietveld options to force specific codereview."""
2941 parser.codereview_group = optparse.OptionGroup(
2942 parser, 'EXPERIMENTAL! Codereview override options')
2943 parser.add_option_group(parser.codereview_group)
2944 parser.codereview_group.add_option(
2945 '--gerrit', action='store_true',
2946 help='Force the use of Gerrit for codereview')
2947 parser.codereview_group.add_option(
2948 '--rietveld', action='store_true',
2949 help='Force the use of Rietveld for codereview')
2950
2951
2952def _process_codereview_select_options(parser, options):
2953 if options.gerrit and options.rietveld:
2954 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2955 options.forced_codereview = None
2956 if options.gerrit:
2957 options.forced_codereview = 'gerrit'
2958 elif options.rietveld:
2959 options.forced_codereview = 'rietveld'
2960
2961
tandriif9aefb72016-07-01 09:06:51 -07002962def _get_bug_line_values(default_project, bugs):
2963 """Given default_project and comma separated list of bugs, yields bug line
2964 values.
2965
2966 Each bug can be either:
2967 * a number, which is combined with default_project
2968 * string, which is left as is.
2969
2970 This function may produce more than one line, because bugdroid expects one
2971 project per line.
2972
2973 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2974 ['v8:123', 'chromium:789']
2975 """
2976 default_bugs = []
2977 others = []
2978 for bug in bugs.split(','):
2979 bug = bug.strip()
2980 if bug:
2981 try:
2982 default_bugs.append(int(bug))
2983 except ValueError:
2984 others.append(bug)
2985
2986 if default_bugs:
2987 default_bugs = ','.join(map(str, default_bugs))
2988 if default_project:
2989 yield '%s:%s' % (default_project, default_bugs)
2990 else:
2991 yield default_bugs
2992 for other in sorted(others):
2993 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2994 yield other
2995
2996
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002997class ChangeDescription(object):
2998 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002999 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003000 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003002 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003003
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003004 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003005 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003006
agable@chromium.org42c20792013-09-12 17:34:49 +00003007 @property # www.logilab.org/ticket/89786
3008 def description(self): # pylint: disable=E0202
3009 return '\n'.join(self._description_lines)
3010
3011 def set_description(self, desc):
3012 if isinstance(desc, basestring):
3013 lines = desc.splitlines()
3014 else:
3015 lines = [line.rstrip() for line in desc]
3016 while lines and not lines[0]:
3017 lines.pop(0)
3018 while lines and not lines[-1]:
3019 lines.pop(-1)
3020 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003021
piman@chromium.org336f9122014-09-04 02:16:55 +00003022 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003023 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003024 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003025 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003026 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003027 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003028
agable@chromium.org42c20792013-09-12 17:34:49 +00003029 # Get the set of R= and TBR= lines and remove them from the desciption.
3030 regexp = re.compile(self.R_LINE)
3031 matches = [regexp.match(line) for line in self._description_lines]
3032 new_desc = [l for i, l in enumerate(self._description_lines)
3033 if not matches[i]]
3034 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003035
agable@chromium.org42c20792013-09-12 17:34:49 +00003036 # Construct new unified R= and TBR= lines.
3037 r_names = []
3038 tbr_names = []
3039 for match in matches:
3040 if not match:
3041 continue
3042 people = cleanup_list([match.group(2).strip()])
3043 if match.group(1) == 'TBR':
3044 tbr_names.extend(people)
3045 else:
3046 r_names.extend(people)
3047 for name in r_names:
3048 if name not in reviewers:
3049 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003050 if add_owners_tbr:
3051 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003052 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003053 all_reviewers = set(tbr_names + reviewers)
3054 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3055 all_reviewers)
3056 tbr_names.extend(owners_db.reviewers_for(missing_files,
3057 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003058 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3059 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3060
3061 # Put the new lines in the description where the old first R= line was.
3062 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3063 if 0 <= line_loc < len(self._description_lines):
3064 if new_tbr_line:
3065 self._description_lines.insert(line_loc, new_tbr_line)
3066 if new_r_line:
3067 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003068 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003069 if new_r_line:
3070 self.append_footer(new_r_line)
3071 if new_tbr_line:
3072 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003073
tandriif9aefb72016-07-01 09:06:51 -07003074 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 self.set_description([
3077 '# Enter a description of the change.',
3078 '# This will be displayed on the codereview site.',
3079 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003080 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003081 '--------------------',
3082 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083
agable@chromium.org42c20792013-09-12 17:34:49 +00003084 regexp = re.compile(self.BUG_LINE)
3085 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003086 prefix = settings.GetBugPrefix()
3087 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3088 for value in values:
3089 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3090 self.append_footer('BUG=%s' % value)
3091
agable@chromium.org42c20792013-09-12 17:34:49 +00003092 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003093 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003094 if not content:
3095 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003096 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003097
3098 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003099 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3100 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003101 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003102 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003103
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003104 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003105 """Adds a footer line to the description.
3106
3107 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3108 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3109 that Gerrit footers are always at the end.
3110 """
3111 parsed_footer_line = git_footers.parse_footer(line)
3112 if parsed_footer_line:
3113 # Line is a gerrit footer in the form: Footer-Key: any value.
3114 # Thus, must be appended observing Gerrit footer rules.
3115 self.set_description(
3116 git_footers.add_footer(self.description,
3117 key=parsed_footer_line[0],
3118 value=parsed_footer_line[1]))
3119 return
3120
3121 if not self._description_lines:
3122 self._description_lines.append(line)
3123 return
3124
3125 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3126 if gerrit_footers:
3127 # git_footers.split_footers ensures that there is an empty line before
3128 # actual (gerrit) footers, if any. We have to keep it that way.
3129 assert top_lines and top_lines[-1] == ''
3130 top_lines, separator = top_lines[:-1], top_lines[-1:]
3131 else:
3132 separator = [] # No need for separator if there are no gerrit_footers.
3133
3134 prev_line = top_lines[-1] if top_lines else ''
3135 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3136 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3137 top_lines.append('')
3138 top_lines.append(line)
3139 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003140
tandrii99a72f22016-08-17 14:33:24 -07003141 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003142 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003143 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003144 reviewers = [match.group(2).strip()
3145 for match in matches
3146 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003147 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003148
bradnelsond975b302016-10-23 12:20:23 -07003149 def get_cced(self):
3150 """Retrieves the list of reviewers."""
3151 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3152 cced = [match.group(2).strip() for match in matches if match]
3153 return cleanup_list(cced)
3154
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003155 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3156 """Updates this commit description given the parent.
3157
3158 This is essentially what Gnumbd used to do.
3159 Consult https://goo.gl/WMmpDe for more details.
3160 """
3161 assert parent_msg # No, orphan branch creation isn't supported.
3162 assert parent_hash
3163 assert dest_ref
3164 parent_footer_map = git_footers.parse_footers(parent_msg)
3165 # This will also happily parse svn-position, which GnumbD is no longer
3166 # supporting. While we'd generate correct footers, the verifier plugin
3167 # installed in Gerrit will block such commit (ie git push below will fail).
3168 parent_position = git_footers.get_position(parent_footer_map)
3169
3170 # Cherry-picks may have last line obscuring their prior footers,
3171 # from git_footers perspective. This is also what Gnumbd did.
3172 cp_line = None
3173 if (self._description_lines and
3174 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3175 cp_line = self._description_lines.pop()
3176
3177 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3178
3179 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3180 # user interference with actual footers we'd insert below.
3181 for i, (k, v) in enumerate(parsed_footers):
3182 if k.startswith('Cr-'):
3183 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3184
3185 # Add Position and Lineage footers based on the parent.
3186 lineage = parent_footer_map.get('Cr-Branched-From', [])
3187 if parent_position[0] == dest_ref:
3188 # Same branch as parent.
3189 number = int(parent_position[1]) + 1
3190 else:
3191 number = 1 # New branch, and extra lineage.
3192 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3193 int(parent_position[1])))
3194
3195 parsed_footers.append(('Cr-Commit-Position',
3196 '%s@{#%d}' % (dest_ref, number)))
3197 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3198
3199 self._description_lines = top_lines
3200 if cp_line:
3201 self._description_lines.append(cp_line)
3202 if self._description_lines[-1] != '':
3203 self._description_lines.append('') # Ensure footer separator.
3204 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3205
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003206
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003207def get_approving_reviewers(props):
3208 """Retrieves the reviewers that approved a CL from the issue properties with
3209 messages.
3210
3211 Note that the list may contain reviewers that are not committer, thus are not
3212 considered by the CQ.
3213 """
3214 return sorted(
3215 set(
3216 message['sender']
3217 for message in props['messages']
3218 if message['approval'] and message['sender'] in props['reviewers']
3219 )
3220 )
3221
3222
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003223def FindCodereviewSettingsFile(filename='codereview.settings'):
3224 """Finds the given file starting in the cwd and going up.
3225
3226 Only looks up to the top of the repository unless an
3227 'inherit-review-settings-ok' file exists in the root of the repository.
3228 """
3229 inherit_ok_file = 'inherit-review-settings-ok'
3230 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003231 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3233 root = '/'
3234 while True:
3235 if filename in os.listdir(cwd):
3236 if os.path.isfile(os.path.join(cwd, filename)):
3237 return open(os.path.join(cwd, filename))
3238 if cwd == root:
3239 break
3240 cwd = os.path.dirname(cwd)
3241
3242
3243def LoadCodereviewSettingsFromFile(fileobj):
3244 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003245 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247 def SetProperty(name, setting, unset_error_ok=False):
3248 fullname = 'rietveld.' + name
3249 if setting in keyvals:
3250 RunGit(['config', fullname, keyvals[setting]])
3251 else:
3252 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3253
tandrii48df5812016-10-17 03:55:37 -07003254 if not keyvals.get('GERRIT_HOST', False):
3255 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256 # Only server setting is required. Other settings can be absent.
3257 # In that case, we ignore errors raised during option deletion attempt.
3258 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003259 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003260 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3261 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003262 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003263 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003264 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3265 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003266 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003267 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003268 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003269 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3270 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003271
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003272 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003273 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003274
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003275 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003276 RunGit(['config', 'gerrit.squash-uploads',
3277 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003278
tandrii@chromium.org28253532016-04-14 13:46:56 +00003279 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003280 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003281 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3284 #should be of the form
3285 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3286 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3287 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3288 keyvals['ORIGIN_URL_CONFIG']])
3289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003290
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003291def urlretrieve(source, destination):
3292 """urllib is broken for SSL connections via a proxy therefore we
3293 can't use urllib.urlretrieve()."""
3294 with open(destination, 'w') as f:
3295 f.write(urllib2.urlopen(source).read())
3296
3297
ukai@chromium.org712d6102013-11-27 00:52:58 +00003298def hasSheBang(fname):
3299 """Checks fname is a #! script."""
3300 with open(fname) as f:
3301 return f.read(2).startswith('#!')
3302
3303
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003304# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3305def DownloadHooks(*args, **kwargs):
3306 pass
3307
3308
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003309def DownloadGerritHook(force):
3310 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003311
3312 Args:
3313 force: True to update hooks. False to install hooks if not present.
3314 """
3315 if not settings.GetIsGerrit():
3316 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003317 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003318 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3319 if not os.access(dst, os.X_OK):
3320 if os.path.exists(dst):
3321 if not force:
3322 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003323 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003324 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003325 if not hasSheBang(dst):
3326 DieWithError('Not a script: %s\n'
3327 'You need to download from\n%s\n'
3328 'into .git/hooks/commit-msg and '
3329 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003330 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3331 except Exception:
3332 if os.path.exists(dst):
3333 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003334 DieWithError('\nFailed to download hooks.\n'
3335 'You need to download from\n%s\n'
3336 'into .git/hooks/commit-msg and '
3337 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003338
3339
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003340
3341def GetRietveldCodereviewSettingsInteractively():
3342 """Prompt the user for settings."""
3343 server = settings.GetDefaultServerUrl(error_ok=True)
3344 prompt = 'Rietveld server (host[:port])'
3345 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3346 newserver = ask_for_data(prompt + ':')
3347 if not server and not newserver:
3348 newserver = DEFAULT_SERVER
3349 if newserver:
3350 newserver = gclient_utils.UpgradeToHttps(newserver)
3351 if newserver != server:
3352 RunGit(['config', 'rietveld.server', newserver])
3353
3354 def SetProperty(initial, caption, name, is_url):
3355 prompt = caption
3356 if initial:
3357 prompt += ' ("x" to clear) [%s]' % initial
3358 new_val = ask_for_data(prompt + ':')
3359 if new_val == 'x':
3360 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3361 elif new_val:
3362 if is_url:
3363 new_val = gclient_utils.UpgradeToHttps(new_val)
3364 if new_val != initial:
3365 RunGit(['config', 'rietveld.' + name, new_val])
3366
3367 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3368 SetProperty(settings.GetDefaultPrivateFlag(),
3369 'Private flag (rietveld only)', 'private', False)
3370 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3371 'tree-status-url', False)
3372 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3373 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3374 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3375 'run-post-upload-hook', False)
3376
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003377@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003378def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003379 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003380
tandrii5d0a0422016-09-14 06:24:35 -07003381 print('WARNING: git cl config works for Rietveld only')
3382 # TODO(tandrii): remove this once we switch to Gerrit.
3383 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003384 parser.add_option('--activate-update', action='store_true',
3385 help='activate auto-updating [rietveld] section in '
3386 '.git/config')
3387 parser.add_option('--deactivate-update', action='store_true',
3388 help='deactivate auto-updating [rietveld] section in '
3389 '.git/config')
3390 options, args = parser.parse_args(args)
3391
3392 if options.deactivate_update:
3393 RunGit(['config', 'rietveld.autoupdate', 'false'])
3394 return
3395
3396 if options.activate_update:
3397 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3398 return
3399
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003400 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003401 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003402 return 0
3403
3404 url = args[0]
3405 if not url.endswith('codereview.settings'):
3406 url = os.path.join(url, 'codereview.settings')
3407
3408 # Load code review settings and download hooks (if available).
3409 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3410 return 0
3411
3412
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003413def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003414 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003415 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3416 branch = ShortBranchName(branchref)
3417 _, args = parser.parse_args(args)
3418 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003419 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003420 return RunGit(['config', 'branch.%s.base-url' % branch],
3421 error_ok=False).strip()
3422 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003423 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003424 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3425 error_ok=False).strip()
3426
3427
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003428def color_for_status(status):
3429 """Maps a Changelist status to color, for CMDstatus and other tools."""
3430 return {
3431 'unsent': Fore.RED,
3432 'waiting': Fore.BLUE,
3433 'reply': Fore.YELLOW,
3434 'lgtm': Fore.GREEN,
3435 'commit': Fore.MAGENTA,
3436 'closed': Fore.CYAN,
3437 'error': Fore.WHITE,
3438 }.get(status, Fore.WHITE)
3439
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003440
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003441def get_cl_statuses(changes, fine_grained, max_processes=None):
3442 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003443
3444 If fine_grained is true, this will fetch CL statuses from the server.
3445 Otherwise, simply indicate if there's a matching url for the given branches.
3446
3447 If max_processes is specified, it is used as the maximum number of processes
3448 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3449 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003450
3451 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003452 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003453 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003454 upload.verbosity = 0
3455
3456 if fine_grained:
3457 # Process one branch synchronously to work through authentication, then
3458 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003459 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003460 def fetch(cl):
3461 try:
3462 return (cl, cl.GetStatus())
3463 except:
3464 # See http://crbug.com/629863.
3465 logging.exception('failed to fetch status for %s:', cl)
3466 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003467 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003468
tandriiea9514a2016-08-17 12:32:37 -07003469 changes_to_fetch = changes[1:]
3470 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003471 # Exit early if there was only one branch to fetch.
3472 return
3473
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003474 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003475 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003476 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003477 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003478
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003479 fetched_cls = set()
3480 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003481 while True:
3482 try:
3483 row = it.next(timeout=5)
3484 except multiprocessing.TimeoutError:
3485 break
3486
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003487 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003488 yield row
3489
3490 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003491 for cl in set(changes_to_fetch) - fetched_cls:
3492 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003493
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003494 else:
3495 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003496 for cl in changes:
3497 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003498
rmistry@google.com2dd99862015-06-22 12:22:18 +00003499
3500def upload_branch_deps(cl, args):
3501 """Uploads CLs of local branches that are dependents of the current branch.
3502
3503 If the local branch dependency tree looks like:
3504 test1 -> test2.1 -> test3.1
3505 -> test3.2
3506 -> test2.2 -> test3.3
3507
3508 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3509 run on the dependent branches in this order:
3510 test2.1, test3.1, test3.2, test2.2, test3.3
3511
3512 Note: This function does not rebase your local dependent branches. Use it when
3513 you make a change to the parent branch that will not conflict with its
3514 dependent branches, and you would like their dependencies updated in
3515 Rietveld.
3516 """
3517 if git_common.is_dirty_git_tree('upload-branch-deps'):
3518 return 1
3519
3520 root_branch = cl.GetBranch()
3521 if root_branch is None:
3522 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3523 'Get on a branch!')
3524 if not cl.GetIssue() or not cl.GetPatchset():
3525 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3526 'patchset dependencies without an uploaded CL.')
3527
3528 branches = RunGit(['for-each-ref',
3529 '--format=%(refname:short) %(upstream:short)',
3530 'refs/heads'])
3531 if not branches:
3532 print('No local branches found.')
3533 return 0
3534
3535 # Create a dictionary of all local branches to the branches that are dependent
3536 # on it.
3537 tracked_to_dependents = collections.defaultdict(list)
3538 for b in branches.splitlines():
3539 tokens = b.split()
3540 if len(tokens) == 2:
3541 branch_name, tracked = tokens
3542 tracked_to_dependents[tracked].append(branch_name)
3543
vapiera7fbd5a2016-06-16 09:17:49 -07003544 print()
3545 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003546 dependents = []
3547 def traverse_dependents_preorder(branch, padding=''):
3548 dependents_to_process = tracked_to_dependents.get(branch, [])
3549 padding += ' '
3550 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003552 dependents.append(dependent)
3553 traverse_dependents_preorder(dependent, padding)
3554 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003555 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003556
3557 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003558 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003559 return 0
3560
vapiera7fbd5a2016-06-16 09:17:49 -07003561 print('This command will checkout all dependent branches and run '
3562 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003563 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3564
andybons@chromium.org962f9462016-02-03 20:00:42 +00003565 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003566 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003567 args.extend(['-t', 'Updated patchset dependency'])
3568
rmistry@google.com2dd99862015-06-22 12:22:18 +00003569 # Record all dependents that failed to upload.
3570 failures = {}
3571 # Go through all dependents, checkout the branch and upload.
3572 try:
3573 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003574 print()
3575 print('--------------------------------------')
3576 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003577 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003578 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003579 try:
3580 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003581 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003582 failures[dependent_branch] = 1
3583 except: # pylint: disable=W0702
3584 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003585 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003586 finally:
3587 # Swap back to the original root branch.
3588 RunGit(['checkout', '-q', root_branch])
3589
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print()
3591 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003592 for dependent_branch in dependents:
3593 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print(' %s : %s' % (dependent_branch, upload_status))
3595 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003596
3597 return 0
3598
3599
kmarshall3bff56b2016-06-06 18:31:47 -07003600def CMDarchive(parser, args):
3601 """Archives and deletes branches associated with closed changelists."""
3602 parser.add_option(
3603 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003604 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003605 parser.add_option(
3606 '-f', '--force', action='store_true',
3607 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003608 parser.add_option(
3609 '-d', '--dry-run', action='store_true',
3610 help='Skip the branch tagging and removal steps.')
3611 parser.add_option(
3612 '-t', '--notags', action='store_true',
3613 help='Do not tag archived branches. '
3614 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003615
3616 auth.add_auth_options(parser)
3617 options, args = parser.parse_args(args)
3618 if args:
3619 parser.error('Unsupported args: %s' % ' '.join(args))
3620 auth_config = auth.extract_auth_config_from_options(options)
3621
3622 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3623 if not branches:
3624 return 0
3625
vapiera7fbd5a2016-06-16 09:17:49 -07003626 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003627 changes = [Changelist(branchref=b, auth_config=auth_config)
3628 for b in branches.splitlines()]
3629 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3630 statuses = get_cl_statuses(changes,
3631 fine_grained=True,
3632 max_processes=options.maxjobs)
3633 proposal = [(cl.GetBranch(),
3634 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3635 for cl, status in statuses
3636 if status == 'closed']
3637 proposal.sort()
3638
3639 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003640 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003641 return 0
3642
3643 current_branch = GetCurrentBranch()
3644
vapiera7fbd5a2016-06-16 09:17:49 -07003645 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003646 if options.notags:
3647 for next_item in proposal:
3648 print(' ' + next_item[0])
3649 else:
3650 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3651 for next_item in proposal:
3652 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003653
kmarshall9249e012016-08-23 12:02:16 -07003654 # Quit now on precondition failure or if instructed by the user, either
3655 # via an interactive prompt or by command line flags.
3656 if options.dry_run:
3657 print('\nNo changes were made (dry run).\n')
3658 return 0
3659 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003660 print('You are currently on a branch \'%s\' which is associated with a '
3661 'closed codereview issue, so archive cannot proceed. Please '
3662 'checkout another branch and run this command again.' %
3663 current_branch)
3664 return 1
kmarshall9249e012016-08-23 12:02:16 -07003665 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003666 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3667 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003668 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003669 return 1
3670
3671 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003672 if not options.notags:
3673 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003674 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003675
vapiera7fbd5a2016-06-16 09:17:49 -07003676 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003677
3678 return 0
3679
3680
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003682 """Show status of changelists.
3683
3684 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003685 - Red not sent for review or broken
3686 - Blue waiting for review
3687 - Yellow waiting for you to reply to review
3688 - Green LGTM'ed
3689 - Magenta in the commit queue
3690 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003691
3692 Also see 'git cl comments'.
3693 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003694 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003695 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003696 parser.add_option('-f', '--fast', action='store_true',
3697 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003698 parser.add_option(
3699 '-j', '--maxjobs', action='store', type=int,
3700 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003701
3702 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003703 _add_codereview_issue_select_options(
3704 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003705 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003706 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003707 if args:
3708 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003709 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710
iannuccie53c9352016-08-17 14:40:40 -07003711 if options.issue is not None and not options.field:
3712 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003713
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003714 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003715 cl = Changelist(auth_config=auth_config, issue=options.issue,
3716 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719 elif options.field == 'id':
3720 issueid = cl.GetIssue()
3721 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723 elif options.field == 'patch':
3724 patchset = cl.GetPatchset()
3725 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003727 elif options.field == 'status':
3728 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729 elif options.field == 'url':
3730 url = cl.GetIssueURL()
3731 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003732 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003733 return 0
3734
3735 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3736 if not branches:
3737 print('No local branch found.')
3738 return 0
3739
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003740 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003741 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003742 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003743 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003744 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003745 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003746 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003747
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003748 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003749 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3750 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3751 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003752 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003753 c, status = output.next()
3754 branch_statuses[c.GetBranch()] = status
3755 status = branch_statuses.pop(branch)
3756 url = cl.GetIssueURL()
3757 if url and (not status or status == 'error'):
3758 # The issue probably doesn't exist anymore.
3759 url += ' (broken)'
3760
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003761 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003762 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003763 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003764 color = ''
3765 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003766 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003767 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003768 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003769 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003770
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003771 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003772 print()
3773 print('Current branch:',)
3774 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003775 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003776 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003777 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003778 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003779 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003780 print('Issue description:')
3781 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003782 return 0
3783
3784
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003785def colorize_CMDstatus_doc():
3786 """To be called once in main() to add colors to git cl status help."""
3787 colors = [i for i in dir(Fore) if i[0].isupper()]
3788
3789 def colorize_line(line):
3790 for color in colors:
3791 if color in line.upper():
3792 # Extract whitespaces first and the leading '-'.
3793 indent = len(line) - len(line.lstrip(' ')) + 1
3794 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3795 return line
3796
3797 lines = CMDstatus.__doc__.splitlines()
3798 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3799
3800
phajdan.jre328cf92016-08-22 04:12:17 -07003801def write_json(path, contents):
3802 with open(path, 'w') as f:
3803 json.dump(contents, f)
3804
3805
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003806@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003807def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003808 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003809
3810 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003811 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003812 parser.add_option('-r', '--reverse', action='store_true',
3813 help='Lookup the branch(es) for the specified issues. If '
3814 'no issues are specified, all branches with mapped '
3815 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003816 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003817 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003818 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003819 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820
dnj@chromium.org406c4402015-03-03 17:22:28 +00003821 if options.reverse:
3822 branches = RunGit(['for-each-ref', 'refs/heads',
3823 '--format=%(refname:short)']).splitlines()
3824
3825 # Reverse issue lookup.
3826 issue_branch_map = {}
3827 for branch in branches:
3828 cl = Changelist(branchref=branch)
3829 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3830 if not args:
3831 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003832 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003833 for issue in args:
3834 if not issue:
3835 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003836 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003837 print('Branch for issue number %s: %s' % (
3838 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003839 if options.json:
3840 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003841 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003842 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003843 if len(args) > 0:
3844 try:
3845 issue = int(args[0])
3846 except ValueError:
3847 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003848 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003849 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003851 if options.json:
3852 write_json(options.json, {
3853 'issue': cl.GetIssue(),
3854 'issue_url': cl.GetIssueURL(),
3855 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003856 return 0
3857
3858
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003859def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003860 """Shows or posts review comments for any changelist."""
3861 parser.add_option('-a', '--add-comment', dest='comment',
3862 help='comment to add to an issue')
3863 parser.add_option('-i', dest='issue',
3864 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003865 parser.add_option('-j', '--json-file',
3866 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003867 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003868 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003869 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003870
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003871 issue = None
3872 if options.issue:
3873 try:
3874 issue = int(options.issue)
3875 except ValueError:
3876 DieWithError('A review issue id is expected to be a number')
3877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003878 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003879
3880 if options.comment:
3881 cl.AddComment(options.comment)
3882 return 0
3883
3884 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003885 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003886 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003887 summary.append({
3888 'date': message['date'],
3889 'lgtm': False,
3890 'message': message['text'],
3891 'not_lgtm': False,
3892 'sender': message['sender'],
3893 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003894 if message['disapproval']:
3895 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003896 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003897 elif message['approval']:
3898 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003899 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003900 elif message['sender'] == data['owner_email']:
3901 color = Fore.MAGENTA
3902 else:
3903 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003904 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003905 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003906 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003907 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003909 if options.json_file:
3910 with open(options.json_file, 'wb') as f:
3911 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003912 return 0
3913
3914
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003915@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003916def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003917 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003918 parser.add_option('-d', '--display', action='store_true',
3919 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003920 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003921 help='New description to set for this issue (- for stdin, '
3922 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003923 parser.add_option('-f', '--force', action='store_true',
3924 help='Delete any unpublished Gerrit edits for this issue '
3925 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003926
3927 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003928 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003929 options, args = parser.parse_args(args)
3930 _process_codereview_select_options(parser, options)
3931
3932 target_issue = None
3933 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003934 target_issue = ParseIssueNumberArgument(args[0])
3935 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003936 parser.print_help()
3937 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003938
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003939 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003940
martiniss6eda05f2016-06-30 10:18:35 -07003941 kwargs = {
3942 'auth_config': auth_config,
3943 'codereview': options.forced_codereview,
3944 }
3945 if target_issue:
3946 kwargs['issue'] = target_issue.issue
3947 if options.forced_codereview == 'rietveld':
3948 kwargs['rietveld_server'] = target_issue.hostname
3949
3950 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003951
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003952 if not cl.GetIssue():
3953 DieWithError('This branch has no associated changelist.')
3954 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003955
smut@google.com34fb6b12015-07-13 20:03:26 +00003956 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003957 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003958 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003959
3960 if options.new_description:
3961 text = options.new_description
3962 if text == '-':
3963 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003964 elif text == '+':
3965 base_branch = cl.GetCommonAncestorWithUpstream()
3966 change = cl.GetChange(base_branch, None, local_description=True)
3967 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003968
3969 description.set_description(text)
3970 else:
3971 description.prompt()
3972
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003973 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003974 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003975 return 0
3976
3977
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003978def CreateDescriptionFromLog(args):
3979 """Pulls out the commit log to use as a base for the CL description."""
3980 log_args = []
3981 if len(args) == 1 and not args[0].endswith('.'):
3982 log_args = [args[0] + '..']
3983 elif len(args) == 1 and args[0].endswith('...'):
3984 log_args = [args[0][:-1]]
3985 elif len(args) == 2:
3986 log_args = [args[0] + '..' + args[1]]
3987 else:
3988 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003989 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003990
3991
thestig@chromium.org44202a22014-03-11 19:22:18 +00003992def CMDlint(parser, args):
3993 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003994 parser.add_option('--filter', action='append', metavar='-x,+y',
3995 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003996 auth.add_auth_options(parser)
3997 options, args = parser.parse_args(args)
3998 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003999
4000 # Access to a protected member _XX of a client class
4001 # pylint: disable=W0212
4002 try:
4003 import cpplint
4004 import cpplint_chromium
4005 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004006 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004007 return 1
4008
4009 # Change the current working directory before calling lint so that it
4010 # shows the correct base.
4011 previous_cwd = os.getcwd()
4012 os.chdir(settings.GetRoot())
4013 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004014 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004015 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4016 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004017 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004018 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004019 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004020
4021 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004022 command = args + files
4023 if options.filter:
4024 command = ['--filter=' + ','.join(options.filter)] + command
4025 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004026
4027 white_regex = re.compile(settings.GetLintRegex())
4028 black_regex = re.compile(settings.GetLintIgnoreRegex())
4029 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4030 for filename in filenames:
4031 if white_regex.match(filename):
4032 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004033 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004034 else:
4035 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4036 extra_check_functions)
4037 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004038 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004039 finally:
4040 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004041 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004042 if cpplint._cpplint_state.error_count != 0:
4043 return 1
4044 return 0
4045
4046
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004047def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004048 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004049 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004051 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004052 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004053 auth.add_auth_options(parser)
4054 options, args = parser.parse_args(args)
4055 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004056
sbc@chromium.org71437c02015-04-09 19:29:40 +00004057 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004058 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059 return 1
4060
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004061 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062 if args:
4063 base_branch = args[0]
4064 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004065 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004066 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004067
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004068 cl.RunHook(
4069 committing=not options.upload,
4070 may_prompt=False,
4071 verbose=options.verbose,
4072 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004073 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074
4075
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004076def GenerateGerritChangeId(message):
4077 """Returns Ixxxxxx...xxx change id.
4078
4079 Works the same way as
4080 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4081 but can be called on demand on all platforms.
4082
4083 The basic idea is to generate git hash of a state of the tree, original commit
4084 message, author/committer info and timestamps.
4085 """
4086 lines = []
4087 tree_hash = RunGitSilent(['write-tree'])
4088 lines.append('tree %s' % tree_hash.strip())
4089 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4090 if code == 0:
4091 lines.append('parent %s' % parent.strip())
4092 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4093 lines.append('author %s' % author.strip())
4094 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4095 lines.append('committer %s' % committer.strip())
4096 lines.append('')
4097 # Note: Gerrit's commit-hook actually cleans message of some lines and
4098 # whitespace. This code is not doing this, but it clearly won't decrease
4099 # entropy.
4100 lines.append(message)
4101 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4102 stdin='\n'.join(lines))
4103 return 'I%s' % change_hash.strip()
4104
4105
wittman@chromium.org455dc922015-01-26 20:15:50 +00004106def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4107 """Computes the remote branch ref to use for the CL.
4108
4109 Args:
4110 remote (str): The git remote for the CL.
4111 remote_branch (str): The git remote branch for the CL.
4112 target_branch (str): The target branch specified by the user.
4113 pending_prefix (str): The pending prefix from the settings.
4114 """
4115 if not (remote and remote_branch):
4116 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004117
wittman@chromium.org455dc922015-01-26 20:15:50 +00004118 if target_branch:
4119 # Cannonicalize branch references to the equivalent local full symbolic
4120 # refs, which are then translated into the remote full symbolic refs
4121 # below.
4122 if '/' not in target_branch:
4123 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4124 else:
4125 prefix_replacements = (
4126 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4127 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4128 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4129 )
4130 match = None
4131 for regex, replacement in prefix_replacements:
4132 match = re.search(regex, target_branch)
4133 if match:
4134 remote_branch = target_branch.replace(match.group(0), replacement)
4135 break
4136 if not match:
4137 # This is a branch path but not one we recognize; use as-is.
4138 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004139 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4140 # Handle the refs that need to land in different refs.
4141 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004142
wittman@chromium.org455dc922015-01-26 20:15:50 +00004143 # Create the true path to the remote branch.
4144 # Does the following translation:
4145 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4146 # * refs/remotes/origin/master -> refs/heads/master
4147 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4148 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4149 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4150 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4151 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4152 'refs/heads/')
4153 elif remote_branch.startswith('refs/remotes/branch-heads'):
4154 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4155 # If a pending prefix exists then replace refs/ with it.
4156 if pending_prefix:
4157 remote_branch = remote_branch.replace('refs/', pending_prefix)
4158 return remote_branch
4159
4160
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004161def cleanup_list(l):
4162 """Fixes a list so that comma separated items are put as individual items.
4163
4164 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4165 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4166 """
4167 items = sum((i.split(',') for i in l), [])
4168 stripped_items = (i.strip() for i in items)
4169 return sorted(filter(None, stripped_items))
4170
4171
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004172@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004173def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004174 """Uploads the current changelist to codereview.
4175
4176 Can skip dependency patchset uploads for a branch by running:
4177 git config branch.branch_name.skip-deps-uploads True
4178 To unset run:
4179 git config --unset branch.branch_name.skip-deps-uploads
4180 Can also set the above globally by using the --global flag.
4181 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004182 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4183 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004184 parser.add_option('--bypass-watchlists', action='store_true',
4185 dest='bypass_watchlists',
4186 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004187 parser.add_option('-f', action='store_true', dest='force',
4188 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004189 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004190 parser.add_option('-b', '--bug',
4191 help='pre-populate the bug number(s) for this issue. '
4192 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004193 parser.add_option('--message-file', dest='message_file',
4194 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004195 parser.add_option('-t', dest='title',
4196 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004197 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004198 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004199 help='reviewer email addresses')
4200 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004201 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004202 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004203 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004204 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004205 parser.add_option('--emulate_svn_auto_props',
4206 '--emulate-svn-auto-props',
4207 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004208 dest="emulate_svn_auto_props",
4209 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004210 parser.add_option('-c', '--use-commit-queue', action='store_true',
4211 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004212 parser.add_option('--private', action='store_true',
4213 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004214 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004215 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004216 metavar='TARGET',
4217 help='Apply CL to remote ref TARGET. ' +
4218 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004219 parser.add_option('--squash', action='store_true',
4220 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004221 parser.add_option('--no-squash', action='store_true',
4222 help='Don\'t squash multiple commits into one ' +
4223 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004224 parser.add_option('--topic', default=None,
4225 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004226 parser.add_option('--email', default=None,
4227 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004228 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4229 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004230 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4231 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004232 help='Send the patchset to do a CQ dry run right after '
4233 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004234 parser.add_option('--dependencies', action='store_true',
4235 help='Uploads CLs of all the local branches that depend on '
4236 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004237
rmistry@google.com2dd99862015-06-22 12:22:18 +00004238 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004239 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004240 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004241 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004242 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004243 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004244 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004245
sbc@chromium.org71437c02015-04-09 19:29:40 +00004246 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004247 return 1
4248
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004249 options.reviewers = cleanup_list(options.reviewers)
4250 options.cc = cleanup_list(options.cc)
4251
tandriib80458a2016-06-23 12:20:07 -07004252 if options.message_file:
4253 if options.message:
4254 parser.error('only one of --message and --message-file allowed.')
4255 options.message = gclient_utils.FileRead(options.message_file)
4256 options.message_file = None
4257
tandrii4d0545a2016-07-06 03:56:49 -07004258 if options.cq_dry_run and options.use_commit_queue:
4259 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4260
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004261 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4262 settings.GetIsGerrit()
4263
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004264 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004265 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004266
4267
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004268def IsSubmoduleMergeCommit(ref):
4269 # When submodules are added to the repo, we expect there to be a single
4270 # non-git-svn merge commit at remote HEAD with a signature comment.
4271 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004272 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004273 return RunGit(cmd) != ''
4274
4275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004276def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004277 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004278
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004279 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4280 upstream and closes the issue automatically and atomically.
4281
4282 Otherwise (in case of Rietveld):
4283 Squashes branch into a single commit.
Andrii Shyshkalov06a25022016-11-24 16:47:00 +01004284 Updates commit message with metadata (e.g. pointer to review).
4285 Pushes the code upstream.
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004286 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004287 """
4288 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4289 help='bypass upload presubmit hook')
4290 parser.add_option('-m', dest='message',
4291 help="override review description")
4292 parser.add_option('-f', action='store_true', dest='force',
4293 help="force yes to questions (don't prompt)")
4294 parser.add_option('-c', dest='contributor',
4295 help="external contributor for patch (appended to " +
4296 "description and used as author for git). Should be " +
4297 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004298 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004299 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004300 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004301 auth_config = auth.extract_auth_config_from_options(options)
4302
4303 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004304
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004305 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4306 if cl.IsGerrit():
4307 if options.message:
4308 # This could be implemented, but it requires sending a new patch to
4309 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4310 # Besides, Gerrit has the ability to change the commit message on submit
4311 # automatically, thus there is no need to support this option (so far?).
4312 parser.error('-m MESSAGE option is not supported for Gerrit.')
4313 if options.contributor:
4314 parser.error(
4315 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4316 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4317 'the contributor\'s "name <email>". If you can\'t upload such a '
4318 'commit for review, contact your repository admin and request'
4319 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004320 if not cl.GetIssue():
Aaron Gablea45ee112016-11-22 15:14:38 -08004321 DieWithError('You must upload the change first to Gerrit.\n'
tandrii73449b02016-09-14 06:27:24 -07004322 ' If you would rather have `git cl land` upload '
4323 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004324 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4325 options.verbose)
4326
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004327 current = cl.GetBranch()
4328 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4329 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004330 print()
4331 print('Attempting to push branch %r into another local branch!' % current)
4332 print()
4333 print('Either reparent this branch on top of origin/master:')
4334 print(' git reparent-branch --root')
4335 print()
4336 print('OR run `git rebase-update` if you think the parent branch is ')
4337 print('already committed.')
4338 print()
4339 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004340 return 1
4341
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004342 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004343 # Default to merging against our best guess of the upstream branch.
4344 args = [cl.GetUpstreamBranch()]
4345
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004346 if options.contributor:
4347 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004348 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004349 return 1
4350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004351 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004352 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004353
sbc@chromium.org71437c02015-04-09 19:29:40 +00004354 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355 return 1
4356
4357 # This rev-list syntax means "show all commits not in my branch that
4358 # are in base_branch".
4359 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4360 base_branch]).splitlines()
4361 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004362 print('Base branch "%s" has %d commits '
4363 'not in this branch.' % (base_branch, len(upstream_commits)))
4364 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004365 return 1
4366
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004367 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004368 svn_head = None
4369 if cmd == 'dcommit' or base_has_submodules:
4370 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4371 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004372
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004373 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004374 # If the base_head is a submodule merge commit, the first parent of the
4375 # base_head should be a git-svn commit, which is what we're interested in.
4376 base_svn_head = base_branch
4377 if base_has_submodules:
4378 base_svn_head += '^1'
4379
4380 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004381 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004382 print('This branch has %d additional commits not upstreamed yet.'
4383 % len(extra_commits.splitlines()))
4384 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4385 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004386 return 1
4387
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004388 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004389 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004390 author = None
4391 if options.contributor:
4392 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004393 hook_results = cl.RunHook(
4394 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004395 may_prompt=not options.force,
4396 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004397 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004398 if not hook_results.should_continue():
4399 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004400
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004401 # Check the tree status if the tree status URL is set.
4402 status = GetTreeStatus()
4403 if 'closed' == status:
4404 print('The tree is closed. Please wait for it to reopen. Use '
4405 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4406 return 1
4407 elif 'unknown' == status:
4408 print('Unable to determine tree status. Please verify manually and '
4409 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4410 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004411
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004412 change_desc = ChangeDescription(options.message)
4413 if not change_desc.description and cl.GetIssue():
4414 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004415
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004416 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004417 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004418 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004419 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004420 print('No description set.')
4421 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004422 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004424 # Keep a separate copy for the commit message, because the commit message
4425 # contains the link to the Rietveld issue, while the Rietveld message contains
4426 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004427 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004428 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004429
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004430 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004431 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004432 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004433 # after it. Add a period on a new line to circumvent this. Also add a space
4434 # before the period to make sure that Gitiles continues to correctly resolve
4435 # the URL.
4436 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004438 commit_desc.append_footer('Patch from %s.' % options.contributor)
4439
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004440 print('Description:')
4441 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004442
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004443 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004444 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004445 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004446
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004447 # We want to squash all this branch's commits into one commit with the proper
4448 # description. We do this by doing a "reset --soft" to the base branch (which
4449 # keeps the working copy the same), then dcommitting that. If origin/master
4450 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4451 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004452 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004453 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4454 # Delete the branches if they exist.
4455 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4456 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4457 result = RunGitWithCode(showref_cmd)
4458 if result[0] == 0:
4459 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004460
4461 # We might be in a directory that's present in this branch but not in the
4462 # trunk. Move up to the top of the tree so that git commands that expect a
4463 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004464 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004465 if rel_base_path:
4466 os.chdir(rel_base_path)
4467
4468 # Stuff our change into the merge branch.
4469 # We wrap in a try...finally block so if anything goes wrong,
4470 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004471 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004472 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004473 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004474 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004475 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004476 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004477 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004478 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004479 RunGit(
4480 [
4481 'commit', '--author', options.contributor,
4482 '-m', commit_desc.description,
4483 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004484 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004485 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004486 if base_has_submodules:
4487 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4488 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4489 RunGit(['checkout', CHERRY_PICK_BRANCH])
4490 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004491 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004492 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004493 mirror = settings.GetGitMirror(remote)
4494 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004495 pending_prefix = settings.GetPendingRefPrefix()
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004496
4497 if ShouldGenerateGitNumberFooters():
4498 # TODO(tandrii): run git fetch in a loop + autorebase when there there
4499 # is no pending ref to push to?
4500 logging.debug('Adding git number footers')
4501 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4502 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4503 branch)
4504 # TODO(tandrii): timestamp handling is missing here.
4505 RunGitSilent(['commit', '--amend', '-m', commit_desc.description])
4506 change_desc = ChangeDescription(commit_desc.description)
4507 # If gnumbd is sitll ON and we ultimately push to branch with
4508 # pending_prefix, gnumbd will modify footers we've just inserted with
4509 # 'Original-', which is annoying but still technically correct.
4510
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004511 if not pending_prefix or branch.startswith(pending_prefix):
4512 # If not using refs/pending/heads/* at all, or target ref is already set
4513 # to pending, then push to the target ref directly.
Andrii Shyshkalov813ec3c2016-11-24 17:06:01 +01004514 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4515 # in practise. I really tried to create a new branch tracking
4516 # refs/pending/heads/master directly and git cl land failed long before
4517 # reaching this. Disagree? Comment on http://crbug.com/642493.
4518 if pending_prefix:
4519 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4520 'Grab your .git/config, add instructions how to reproduce '
4521 'this, and post it to http://crbug.com/642493.\n'
4522 'The first reporter gets a free "Black Swan" book from '
4523 'tandrii@\n\n')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004524 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004525 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004526 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004527 else:
4528 # Cherry-pick the change on top of pending ref and then push it.
4529 assert branch.startswith('refs/'), branch
4530 assert pending_prefix[-1] == '/', pending_prefix
4531 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004532 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004533 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004534 if retcode == 0:
4535 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536 else:
4537 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004538 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004539 'svn', 'dcommit',
4540 '-C%s' % options.similarity,
4541 '--no-rebase', '--rmdir',
4542 ]
4543 if settings.GetForceHttpsCommitUrl():
4544 # Allow forcing https commit URLs for some projects that don't allow
4545 # committing to http URLs (like Google Code).
4546 remote_url = cl.GetGitSvnRemoteUrl()
4547 if urlparse.urlparse(remote_url).scheme == 'http':
4548 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004549 cmd_args.append('--commit-url=%s' % remote_url)
4550 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004551 if 'Committed r' in output:
4552 revision = re.match(
4553 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4554 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004555 finally:
4556 # And then swap back to the original branch and clean up.
4557 RunGit(['checkout', '-q', cl.GetBranch()])
4558 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004559 if base_has_submodules:
4560 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004561
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004562 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004563 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004564 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004565
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004566 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004567 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004568 try:
4569 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4570 # We set pushed_to_pending to False, since it made it all the way to the
4571 # real ref.
4572 pushed_to_pending = False
4573 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004574 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004575
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004576 if cl.GetIssue():
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01004577 # TODO(tandrii): figure out story of to pending + git numberer.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004578 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004579 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004580 if not to_pending:
4581 if viewvc_url and revision:
4582 change_desc.append_footer(
4583 'Committed: %s%s' % (viewvc_url, revision))
4584 elif revision:
4585 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004586 print('Closing issue '
4587 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004588 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004590 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004591 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004592 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004593 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004594 if options.bypass_hooks:
4595 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4596 else:
4597 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004598 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004599
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004600 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004601 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004602 print('The commit is in the pending queue (%s).' % pending_ref)
4603 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4604 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004605
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004606 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4607 if os.path.isfile(hook):
4608 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004609
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004610 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004611
4612
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004613def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004614 print()
4615 print('Waiting for commit to be landed on %s...' % real_ref)
4616 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004617 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4618 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004619 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004620
4621 loop = 0
4622 while True:
4623 sys.stdout.write('fetching (%d)... \r' % loop)
4624 sys.stdout.flush()
4625 loop += 1
4626
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004627 if mirror:
4628 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004629 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4630 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4631 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4632 for commit in commits.splitlines():
4633 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004634 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004635 return commit
4636
4637 current_rev = to_rev
4638
4639
tandriibf429402016-09-14 07:09:12 -07004640def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004641 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4642
4643 Returns:
4644 (retcode of last operation, output log of last operation).
4645 """
4646 assert pending_ref.startswith('refs/'), pending_ref
4647 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4648 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4649 code = 0
4650 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004651 max_attempts = 3
4652 attempts_left = max_attempts
4653 while attempts_left:
4654 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004655 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004656 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004657
4658 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004659 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004660 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004661 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004662 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004663 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004664 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004665 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004666 continue
4667
4668 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004669 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004670 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004671 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004672 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004673 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4674 'the following files have merge conflicts:' % pending_ref)
4675 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4676 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004677 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004678 return code, out
4679
4680 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004681 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004682 code, out = RunGitWithCode(
4683 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4684 if code == 0:
4685 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004686 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004687 return code, out
4688
vapiera7fbd5a2016-06-16 09:17:49 -07004689 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004690 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004691 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004692 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004693 print('Fatal push error. Make sure your .netrc credentials and git '
4694 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004695 return code, out
4696
vapiera7fbd5a2016-06-16 09:17:49 -07004697 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004698 return code, out
4699
4700
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004701def IsFatalPushFailure(push_stdout):
4702 """True if retrying push won't help."""
4703 return '(prohibited by Gerrit)' in push_stdout
4704
4705
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004706@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004707def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004708 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004709 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004710 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004711 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004712 message = """This repository appears to be a git-svn mirror, but we
4713don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004714 else:
4715 message = """This doesn't appear to be an SVN repository.
4716If your project has a true, writeable git repository, you probably want to run
4717'git cl land' instead.
4718If your project has a git mirror of an upstream SVN master, you probably need
4719to run 'git svn init'.
4720
4721Using the wrong command might cause your commit to appear to succeed, and the
4722review to be closed, without actually landing upstream. If you choose to
4723proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004724 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004725 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004726 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4727 'Please let us know of this project you are committing to:'
4728 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729 return SendUpstream(parser, args, 'dcommit')
4730
4731
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004732@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004733def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004734 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004735 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004736 print('This appears to be an SVN repository.')
4737 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004738 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004739 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004740 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004741
4742
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004743@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004744def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004745 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004746 parser.add_option('-b', dest='newbranch',
4747 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004748 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004749 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004750 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4751 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004752 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004753 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004754 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004755 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004756 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004757 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004758
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004759
4760 group = optparse.OptionGroup(
4761 parser,
4762 'Options for continuing work on the current issue uploaded from a '
4763 'different clone (e.g. different machine). Must be used independently '
4764 'from the other options. No issue number should be specified, and the '
4765 'branch must have an issue number associated with it')
4766 group.add_option('--reapply', action='store_true', dest='reapply',
4767 help='Reset the branch and reapply the issue.\n'
4768 'CAUTION: This will undo any local changes in this '
4769 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004770
4771 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004772 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004773 parser.add_option_group(group)
4774
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004775 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004776 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004777 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004778 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004779 auth_config = auth.extract_auth_config_from_options(options)
4780
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004781
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004782 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004783 if options.newbranch:
4784 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004785 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004786 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004787
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004788 cl = Changelist(auth_config=auth_config,
4789 codereview=options.forced_codereview)
4790 if not cl.GetIssue():
4791 parser.error('current branch must have an associated issue')
4792
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004793 upstream = cl.GetUpstreamBranch()
4794 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004795 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004796
4797 RunGit(['reset', '--hard', upstream])
4798 if options.pull:
4799 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004800
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004801 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4802 options.directory)
4803
4804 if len(args) != 1 or not args[0]:
4805 parser.error('Must specify issue number or url')
4806
4807 # We don't want uncommitted changes mixed up with the patch.
4808 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004809 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004811 if options.newbranch:
4812 if options.force:
4813 RunGit(['branch', '-D', options.newbranch],
4814 stderr=subprocess2.PIPE, error_ok=True)
4815 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004816 elif not GetCurrentBranch():
4817 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004818
4819 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4820
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004821 if cl.IsGerrit():
4822 if options.reject:
4823 parser.error('--reject is not supported with Gerrit codereview.')
4824 if options.nocommit:
4825 parser.error('--nocommit is not supported with Gerrit codereview.')
4826 if options.directory:
4827 parser.error('--directory is not supported with Gerrit codereview.')
4828
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004829 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004830 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004831
4832
4833def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004834 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004835 # Provide a wrapper for git svn rebase to help avoid accidental
4836 # git svn dcommit.
4837 # It's the only command that doesn't use parser at all since we just defer
4838 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004839
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004840 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004841
4842
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004843def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004844 """Fetches the tree status and returns either 'open', 'closed',
4845 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004846 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004847 if url:
4848 status = urllib2.urlopen(url).read().lower()
4849 if status.find('closed') != -1 or status == '0':
4850 return 'closed'
4851 elif status.find('open') != -1 or status == '1':
4852 return 'open'
4853 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004854 return 'unset'
4855
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004856
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004857def GetTreeStatusReason():
4858 """Fetches the tree status from a json url and returns the message
4859 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004860 url = settings.GetTreeStatusUrl()
4861 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004862 connection = urllib2.urlopen(json_url)
4863 status = json.loads(connection.read())
4864 connection.close()
4865 return status['message']
4866
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004867
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004868def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004869 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004870 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004871 status = GetTreeStatus()
4872 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004873 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004874 return 2
4875
vapiera7fbd5a2016-06-16 09:17:49 -07004876 print('The tree is %s' % status)
4877 print()
4878 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004879 if status != 'open':
4880 return 1
4881 return 0
4882
4883
maruel@chromium.org15192402012-09-06 12:38:29 +00004884def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004885 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004886 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004887 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004888 '-b', '--bot', action='append',
4889 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4890 'times to specify multiple builders. ex: '
4891 '"-b win_rel -b win_layout". See '
4892 'the try server waterfall for the builders name and the tests '
4893 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004894 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004895 '-B', '--bucket', default='',
4896 help=('Buildbucket bucket to send the try requests.'))
4897 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004898 '-m', '--master', default='',
4899 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004900 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004901 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004902 help='Revision to use for the try job; default: the revision will '
4903 'be determined by the try recipe that builder runs, which usually '
4904 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004905 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004906 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004907 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004908 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004909 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004910 '--project',
4911 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004912 'in recipe to determine to which repository or directory to '
4913 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004914 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004915 '-p', '--property', dest='properties', action='append', default=[],
4916 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004917 'key2=value2 etc. The value will be treated as '
4918 'json if decodable, or as string otherwise. '
4919 'NOTE: using this may make your try job not usable for CQ, '
4920 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004921 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004922 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4923 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004924 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004925 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004926 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004927 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004928
machenbach@chromium.org45453142015-09-15 08:45:22 +00004929 # Make sure that all properties are prop=value pairs.
4930 bad_params = [x for x in options.properties if '=' not in x]
4931 if bad_params:
4932 parser.error('Got properties with missing "=": %s' % bad_params)
4933
maruel@chromium.org15192402012-09-06 12:38:29 +00004934 if args:
4935 parser.error('Unknown arguments: %s' % args)
4936
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004937 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004938 if not cl.GetIssue():
4939 parser.error('Need to upload first')
4940
tandriie113dfd2016-10-11 10:20:12 -07004941 error_message = cl.CannotTriggerTryJobReason()
4942 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004943 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004944
borenet6c0efe62016-10-19 08:13:29 -07004945 if options.bucket and options.master:
4946 parser.error('Only one of --bucket and --master may be used.')
4947
qyearsley1fdfcb62016-10-24 13:22:03 -07004948 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004949
qyearsleydd49f942016-10-28 11:57:22 -07004950 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4951 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004952 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004953 if options.verbose:
4954 print('git cl try with no bots now defaults to CQ Dry Run.')
4955 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004956
borenet6c0efe62016-10-19 08:13:29 -07004957 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004958 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004959 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004960 'of bot requires an initial job from a parent (usually a builder). '
4961 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004962 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004963 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004964
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004965 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004966 # TODO(tandrii): Checking local patchset against remote patchset is only
4967 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4968 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004969 print('Warning: Codereview server has newer patchsets (%s) than most '
4970 'recent upload from local checkout (%s). Did a previous upload '
4971 'fail?\n'
4972 'By default, git cl try uses the latest patchset from '
4973 'codereview, continuing to use patchset %s.\n' %
4974 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004975
tandrii568043b2016-10-11 07:49:18 -07004976 try:
borenet6c0efe62016-10-19 08:13:29 -07004977 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4978 patchset)
tandrii568043b2016-10-11 07:49:18 -07004979 except BuildbucketResponseException as ex:
4980 print('ERROR: %s' % ex)
4981 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004982 return 0
4983
4984
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004985def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004986 """Prints info about try jobs associated with current CL."""
4987 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004988 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004989 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004990 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004991 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004992 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004993 '--color', action='store_true', default=setup_color.IS_TTY,
4994 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004995 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004996 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4997 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004998 group.add_option(
4999 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005000 parser.add_option_group(group)
5001 auth.add_auth_options(parser)
5002 options, args = parser.parse_args(args)
5003 if args:
5004 parser.error('Unrecognized args: %s' % ' '.join(args))
5005
5006 auth_config = auth.extract_auth_config_from_options(options)
5007 cl = Changelist(auth_config=auth_config)
5008 if not cl.GetIssue():
5009 parser.error('Need to upload first')
5010
tandrii221ab252016-10-06 08:12:04 -07005011 patchset = options.patchset
5012 if not patchset:
5013 patchset = cl.GetMostRecentPatchset()
5014 if not patchset:
5015 parser.error('Codereview doesn\'t know about issue %s. '
5016 'No access to issue or wrong issue number?\n'
5017 'Either upload first, or pass --patchset explicitely' %
5018 cl.GetIssue())
5019
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005020 # TODO(tandrii): Checking local patchset against remote patchset is only
5021 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5022 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005023 print('Warning: Codereview server has newer patchsets (%s) than most '
5024 'recent upload from local checkout (%s). Did a previous upload '
5025 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005026 'By default, git cl try-results uses the latest patchset from '
5027 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005028 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005029 try:
tandrii221ab252016-10-06 08:12:04 -07005030 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005031 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005032 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005033 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005034 if options.json:
5035 write_try_results_json(options.json, jobs)
5036 else:
5037 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005038 return 0
5039
5040
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005041@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005042def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005043 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005044 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005045 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005046 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005048 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005049 if args:
5050 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005051 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005052 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005053 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005054 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005055
5056 # Clear configured merge-base, if there is one.
5057 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005058 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005059 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005060 return 0
5061
5062
thestig@chromium.org00858c82013-12-02 23:08:03 +00005063def CMDweb(parser, args):
5064 """Opens the current CL in the web browser."""
5065 _, args = parser.parse_args(args)
5066 if args:
5067 parser.error('Unrecognized args: %s' % ' '.join(args))
5068
5069 issue_url = Changelist().GetIssueURL()
5070 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005071 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005072 return 1
5073
5074 webbrowser.open(issue_url)
5075 return 0
5076
5077
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005078def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005079 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005080 parser.add_option('-d', '--dry-run', action='store_true',
5081 help='trigger in dry run mode')
5082 parser.add_option('-c', '--clear', action='store_true',
5083 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005084 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005085 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005086 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005087 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005088 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005089 if args:
5090 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005091 if options.dry_run and options.clear:
5092 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5093
iannuccie53c9352016-08-17 14:40:40 -07005094 cl = Changelist(auth_config=auth_config, issue=options.issue,
5095 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005096 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005097 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005098 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005099 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005100 state = _CQState.DRY_RUN
5101 else:
5102 state = _CQState.COMMIT
5103 if not cl.GetIssue():
5104 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005105 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005106 return 0
5107
5108
groby@chromium.org411034a2013-02-26 15:12:01 +00005109def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005110 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005111 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005112 auth.add_auth_options(parser)
5113 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005114 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005115 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005116 if args:
5117 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005118 cl = Changelist(auth_config=auth_config, issue=options.issue,
5119 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005120 # Ensure there actually is an issue to close.
5121 cl.GetDescription()
5122 cl.CloseIssue()
5123 return 0
5124
5125
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005126def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005127 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005128 parser.add_option(
5129 '--stat',
5130 action='store_true',
5131 dest='stat',
5132 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005133 auth.add_auth_options(parser)
5134 options, args = parser.parse_args(args)
5135 auth_config = auth.extract_auth_config_from_options(options)
5136 if args:
5137 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005138
5139 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005140 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005141 # Staged changes would be committed along with the patch from last
5142 # upload, hence counted toward the "last upload" side in the final
5143 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005144 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005145 return 1
5146
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005147 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005148 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005149 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005150 if not issue:
5151 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005152 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005153 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005154
5155 # Create a new branch based on the merge-base
5156 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005157 # Clear cached branch in cl object, to avoid overwriting original CL branch
5158 # properties.
5159 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005160 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005161 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005162 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005163 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005164 return rtn
5165
wychen@chromium.org06928532015-02-03 02:11:29 +00005166 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005167 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005168 cmd = ['git', 'diff']
5169 if options.stat:
5170 cmd.append('--stat')
5171 cmd.extend([TMP_BRANCH, branch, '--'])
5172 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005173 finally:
5174 RunGit(['checkout', '-q', branch])
5175 RunGit(['branch', '-D', TMP_BRANCH])
5176
5177 return 0
5178
5179
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005180def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005181 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005182 parser.add_option(
5183 '--no-color',
5184 action='store_true',
5185 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005186 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005187 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005188 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005189
5190 author = RunGit(['config', 'user.email']).strip() or None
5191
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005192 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005193
5194 if args:
5195 if len(args) > 1:
5196 parser.error('Unknown args')
5197 base_branch = args[0]
5198 else:
5199 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005200 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005201
5202 change = cl.GetChange(base_branch, None)
5203 return owners_finder.OwnersFinder(
5204 [f.LocalPath() for f in
5205 cl.GetChange(base_branch, None).AffectedFiles()],
5206 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005207 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005208 disable_color=options.no_color).run()
5209
5210
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005211def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005212 """Generates a diff command."""
5213 # Generate diff for the current branch's changes.
5214 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5215 upstream_commit, '--' ]
5216
5217 if args:
5218 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005219 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005220 diff_cmd.append(arg)
5221 else:
5222 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005223
5224 return diff_cmd
5225
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005226def MatchingFileType(file_name, extensions):
5227 """Returns true if the file name ends with one of the given extensions."""
5228 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005229
enne@chromium.org555cfe42014-01-29 18:21:39 +00005230@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005231def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005232 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005233 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005234 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005235 parser.add_option('--full', action='store_true',
5236 help='Reformat the full content of all touched files')
5237 parser.add_option('--dry-run', action='store_true',
5238 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005239 parser.add_option('--python', action='store_true',
5240 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005241 parser.add_option('--diff', action='store_true',
5242 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005243 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005244
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005245 # git diff generates paths against the root of the repository. Change
5246 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005247 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005248 if rel_base_path:
5249 os.chdir(rel_base_path)
5250
digit@chromium.org29e47272013-05-17 17:01:46 +00005251 # Grab the merge-base commit, i.e. the upstream commit of the current
5252 # branch when it was created or the last time it was rebased. This is
5253 # to cover the case where the user may have called "git fetch origin",
5254 # moving the origin branch to a newer commit, but hasn't rebased yet.
5255 upstream_commit = None
5256 cl = Changelist()
5257 upstream_branch = cl.GetUpstreamBranch()
5258 if upstream_branch:
5259 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5260 upstream_commit = upstream_commit.strip()
5261
5262 if not upstream_commit:
5263 DieWithError('Could not find base commit for this branch. '
5264 'Are you in detached state?')
5265
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005266 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5267 diff_output = RunGit(changed_files_cmd)
5268 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005269 # Filter out files deleted by this CL
5270 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005271
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005272 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5273 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5274 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005275 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005276
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005277 top_dir = os.path.normpath(
5278 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5279
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005280 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5281 # formatted. This is used to block during the presubmit.
5282 return_value = 0
5283
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005284 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005285 # Locate the clang-format binary in the checkout
5286 try:
5287 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005288 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005289 DieWithError(e)
5290
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005291 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005292 cmd = [clang_format_tool]
5293 if not opts.dry_run and not opts.diff:
5294 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005295 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005296 if opts.diff:
5297 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005298 else:
5299 env = os.environ.copy()
5300 env['PATH'] = str(os.path.dirname(clang_format_tool))
5301 try:
5302 script = clang_format.FindClangFormatScriptInChromiumTree(
5303 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005304 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005305 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005306
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005307 cmd = [sys.executable, script, '-p0']
5308 if not opts.dry_run and not opts.diff:
5309 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005310
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005311 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5312 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005313
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005314 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5315 if opts.diff:
5316 sys.stdout.write(stdout)
5317 if opts.dry_run and len(stdout) > 0:
5318 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005319
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005320 # Similar code to above, but using yapf on .py files rather than clang-format
5321 # on C/C++ files
5322 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005323 yapf_tool = gclient_utils.FindExecutable('yapf')
5324 if yapf_tool is None:
5325 DieWithError('yapf not found in PATH')
5326
5327 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005328 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005329 cmd = [yapf_tool]
5330 if not opts.dry_run and not opts.diff:
5331 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005332 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005333 if opts.diff:
5334 sys.stdout.write(stdout)
5335 else:
5336 # TODO(sbc): yapf --lines mode still has some issues.
5337 # https://github.com/google/yapf/issues/154
5338 DieWithError('--python currently only works with --full')
5339
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005340 # Dart's formatter does not have the nice property of only operating on
5341 # modified chunks, so hard code full.
5342 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005343 try:
5344 command = [dart_format.FindDartFmtToolInChromiumTree()]
5345 if not opts.dry_run and not opts.diff:
5346 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005347 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005348
ppi@chromium.org6593d932016-03-03 15:41:15 +00005349 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005350 if opts.dry_run and stdout:
5351 return_value = 2
5352 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005353 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5354 'found in this checkout. Files in other languages are still '
5355 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005356
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005357 # Format GN build files. Always run on full build files for canonical form.
5358 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005359 cmd = ['gn', 'format' ]
5360 if opts.dry_run or opts.diff:
5361 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005362 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005363 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5364 shell=sys.platform == 'win32',
5365 cwd=top_dir)
5366 if opts.dry_run and gn_ret == 2:
5367 return_value = 2 # Not formatted.
5368 elif opts.diff and gn_ret == 2:
5369 # TODO this should compute and print the actual diff.
5370 print("This change has GN build file diff for " + gn_diff_file)
5371 elif gn_ret != 0:
5372 # For non-dry run cases (and non-2 return values for dry-run), a
5373 # nonzero error code indicates a failure, probably because the file
5374 # doesn't parse.
5375 DieWithError("gn format failed on " + gn_diff_file +
5376 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005377
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005378 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005379
5380
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005381@subcommand.usage('<codereview url or issue id>')
5382def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005383 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005384 _, args = parser.parse_args(args)
5385
5386 if len(args) != 1:
5387 parser.print_help()
5388 return 1
5389
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005390 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005391 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005392 parser.print_help()
5393 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005394 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005395
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005396 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005397 output = RunGit(['config', '--local', '--get-regexp',
5398 r'branch\..*\.%s' % issueprefix],
5399 error_ok=True)
5400 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005401 if issue == target_issue:
5402 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005403
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005404 branches = []
5405 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005406 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005407 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005408 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005409 return 1
5410 if len(branches) == 1:
5411 RunGit(['checkout', branches[0]])
5412 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005413 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005414 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005415 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005416 which = raw_input('Choose by index: ')
5417 try:
5418 RunGit(['checkout', branches[int(which)]])
5419 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005420 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005421 return 1
5422
5423 return 0
5424
5425
maruel@chromium.org29404b52014-09-08 22:58:00 +00005426def CMDlol(parser, args):
5427 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005428 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005429 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5430 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5431 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005432 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005433 return 0
5434
5435
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005436class OptionParser(optparse.OptionParser):
5437 """Creates the option parse and add --verbose support."""
5438 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005439 optparse.OptionParser.__init__(
5440 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005441 self.add_option(
5442 '-v', '--verbose', action='count', default=0,
5443 help='Use 2 times for more debugging info')
5444
5445 def parse_args(self, args=None, values=None):
5446 options, args = optparse.OptionParser.parse_args(self, args, values)
5447 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5448 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5449 return options, args
5450
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005452def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005453 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005454 print('\nYour python version %s is unsupported, please upgrade.\n' %
5455 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005456 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005457
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005458 # Reload settings.
5459 global settings
5460 settings = Settings()
5461
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005462 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005463 dispatcher = subcommand.CommandDispatcher(__name__)
5464 try:
5465 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005466 except auth.AuthenticationError as e:
5467 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005468 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005469 if e.code != 500:
5470 raise
5471 DieWithError(
5472 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5473 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005474 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005475
5476
5477if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005478 # These affect sys.stdout so do it outside of main() to simplify mocks in
5479 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005480 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005481 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005482 try:
5483 sys.exit(main(sys.argv[1:]))
5484 except KeyboardInterrupt:
5485 sys.stderr.write('interrupted\n')
5486 sys.exit(1)