blob: 65c7fc79f981c80f980187f4555f7c8437203594 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +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
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000013import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000014import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import logging
16import optparse
17import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000018import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000020import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000022import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import textwrap
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000025import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000026import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000027import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028
29try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000030 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031except ImportError:
32 pass
33
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000035from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036from third_party import upload
37import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000038import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000039import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000040import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000041import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000042import git_common
piman@chromium.org336f9122014-09-04 02:16:55 +000043import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000044import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000046import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000048import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000049import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050import watchlists
51
maruel@chromium.org0633fb42013-08-16 20:06:14 +000052__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000054DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000055POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000057GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000058CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000059REFS_THAT_ALIAS_TO_OTHER_REFS = {
60 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
61 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
62}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063
thestig@chromium.org44202a22014-03-11 19:22:18 +000064# Valid extensions for files we want to lint.
65DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
66DEFAULT_LINT_IGNORE_REGEX = r"$^"
67
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000068# Shortcut since it quickly becomes redundant.
69Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000070
maruel@chromium.orgddd59412011-11-30 14:20:38 +000071# Initialized in main()
72settings = None
73
74
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000076 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000077 sys.exit(1)
78
79
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000080def GetNoGitPagerEnv():
81 env = os.environ.copy()
82 # 'cat' is a magical git string that disables pagers on all platforms.
83 env['GIT_PAGER'] = 'cat'
84 return env
85
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000086
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000087def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000089 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000090 except subprocess2.CalledProcessError as e:
91 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000092 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000094 'Command "%s" failed.\n%s' % (
95 ' '.join(args), error_message or e.stdout or ''))
96 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097
98
99def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000101 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102
103
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000104def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000106 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000107 if suppress_stderr:
108 stderr = subprocess2.VOID
109 else:
110 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000111 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000112 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113 stdout=subprocess2.PIPE,
114 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 return code, out[0]
116 except ValueError:
117 # When the subprocess fails, it returns None. That triggers a ValueError
118 # when trying to unpack the return value into (out, code).
119 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000120
121
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000122def RunGitSilent(args):
123 """Returns stdout, suppresses stderr and ingores the return code."""
124 return RunGitWithCode(args, suppress_stderr=True)[1]
125
126
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000127def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000128 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000129 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000130 return (version.startswith(prefix) and
131 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000132
133
maruel@chromium.org90541732011-04-01 17:54:18 +0000134def ask_for_data(prompt):
135 try:
136 return raw_input(prompt)
137 except KeyboardInterrupt:
138 # Hide the exception.
139 sys.exit(1)
140
141
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142def git_set_branch_value(key, value):
143 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000144 if not branch:
145 return
146
147 cmd = ['config']
148 if isinstance(value, int):
149 cmd.append('--int')
150 git_key = 'branch.%s.%s' % (branch, key)
151 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000152
153
154def git_get_branch_default(key, default):
155 branch = Changelist().GetBranch()
156 if branch:
157 git_key = 'branch.%s.%s' % (branch, key)
158 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
159 try:
160 return int(stdout.strip())
161 except ValueError:
162 pass
163 return default
164
165
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000166def add_git_similarity(parser):
167 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 help='Sets the percentage that a pair of files need to match in order to'
170 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171 parser.add_option(
172 '--find-copies', action='store_true',
173 help='Allows git to look for copies.')
174 parser.add_option(
175 '--no-find-copies', action='store_false', dest='find_copies',
176 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000177
178 old_parser_args = parser.parse_args
179 def Parse(args):
180 options, args = old_parser_args(args)
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000183 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 print('Note: Saving similarity of %d%% in git config.'
186 % options.similarity)
187 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000188
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 options.similarity = max(0, min(options.similarity, 100))
190
191 if options.find_copies is None:
192 options.find_copies = bool(
193 git_get_branch_default('git-find-copies', True))
194 else:
195 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196
197 print('Using %d%% similarity for rename/copy detection. '
198 'Override with --similarity.' % options.similarity)
199
200 return options, args
201 parser.parse_args = Parse
202
203
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000204def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
205 """Return the corresponding git ref if |base_url| together with |glob_spec|
206 matches the full |url|.
207
208 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
209 """
210 fetch_suburl, as_ref = glob_spec.split(':')
211 if allow_wildcards:
212 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
213 if glob_match:
214 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
215 # "branches/{472,597,648}/src:refs/remotes/svn/*".
216 branch_re = re.escape(base_url)
217 if glob_match.group(1):
218 branch_re += '/' + re.escape(glob_match.group(1))
219 wildcard = glob_match.group(2)
220 if wildcard == '*':
221 branch_re += '([^/]*)'
222 else:
223 # Escape and replace surrounding braces with parentheses and commas
224 # with pipe symbols.
225 wildcard = re.escape(wildcard)
226 wildcard = re.sub('^\\\\{', '(', wildcard)
227 wildcard = re.sub('\\\\,', '|', wildcard)
228 wildcard = re.sub('\\\\}$', ')', wildcard)
229 branch_re += wildcard
230 if glob_match.group(3):
231 branch_re += re.escape(glob_match.group(3))
232 match = re.match(branch_re, url)
233 if match:
234 return re.sub('\*$', match.group(1), as_ref)
235
236 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
237 if fetch_suburl:
238 full_url = base_url + '/' + fetch_suburl
239 else:
240 full_url = base_url
241 if full_url == url:
242 return as_ref
243 return None
244
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000245
iannucci@chromium.org79540052012-10-19 23:15:26 +0000246def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000247 """Prints statistics about the change to the user."""
248 # --no-ext-diff is broken in some versions of Git, so try to work around
249 # this by overriding the environment (but there is still a problem if the
250 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000251 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252 if 'GIT_EXTERNAL_DIFF' in env:
253 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000254
255 if find_copies:
256 similarity_options = ['--find-copies-harder', '-l100000',
257 '-C%s' % similarity]
258 else:
259 similarity_options = ['-M%s' % similarity]
260
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000261 try:
262 stdout = sys.stdout.fileno()
263 except AttributeError:
264 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000265 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000266 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000267 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000268 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000269
270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000271class Settings(object):
272 def __init__(self):
273 self.default_server = None
274 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000275 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 self.is_git_svn = None
277 self.svn_branch = None
278 self.tree_status_url = None
279 self.viewvc_url = None
280 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000281 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000282 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000283 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000284 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000285 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286
287 def LazyUpdateIfNeeded(self):
288 """Updates the settings from a codereview.settings file, if available."""
289 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000290 # The only value that actually changes the behavior is
291 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000292 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000293 error_ok=True
294 ).strip().lower()
295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000297 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000298 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000299 # set updated to True to avoid infinite calling loop
300 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000301 self.updated = True
302 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303 self.updated = True
304
305 def GetDefaultServerUrl(self, error_ok=False):
306 if not self.default_server:
307 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000308 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000309 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 if error_ok:
311 return self.default_server
312 if not self.default_server:
313 error_message = ('Could not find settings file. You must configure '
314 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000315 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000316 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000317 return self.default_server
318
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000319 @staticmethod
320 def GetRelativeRoot():
321 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000323 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000324 if self.root is None:
325 self.root = os.path.abspath(self.GetRelativeRoot())
326 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000327
328 def GetIsGitSvn(self):
329 """Return true if this repo looks like it's using git-svn."""
330 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000331 if self.GetPendingRefPrefix():
332 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
333 self.is_git_svn = False
334 else:
335 # If you have any "svn-remote.*" config keys, we think you're using svn.
336 self.is_git_svn = RunGitWithCode(
337 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000338 return self.is_git_svn
339
340 def GetSVNBranch(self):
341 if self.svn_branch is None:
342 if not self.GetIsGitSvn():
343 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
344
345 # Try to figure out which remote branch we're based on.
346 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000347 # 1) iterate through our branch history and find the svn URL.
348 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000349
350 # regexp matching the git-svn line that contains the URL.
351 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
352
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000353 # We don't want to go through all of history, so read a line from the
354 # pipe at a time.
355 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000356 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000357 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
358 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000359 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000360 for line in proc.stdout:
361 match = git_svn_re.match(line)
362 if match:
363 url = match.group(1)
364 proc.stdout.close() # Cut pipe.
365 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000366
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000367 if url:
368 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
369 remotes = RunGit(['config', '--get-regexp',
370 r'^svn-remote\..*\.url']).splitlines()
371 for remote in remotes:
372 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000373 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000374 remote = match.group(1)
375 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000376 rewrite_root = RunGit(
377 ['config', 'svn-remote.%s.rewriteRoot' % remote],
378 error_ok=True).strip()
379 if rewrite_root:
380 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000381 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000382 ['config', 'svn-remote.%s.fetch' % remote],
383 error_ok=True).strip()
384 if fetch_spec:
385 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
386 if self.svn_branch:
387 break
388 branch_spec = RunGit(
389 ['config', 'svn-remote.%s.branches' % remote],
390 error_ok=True).strip()
391 if branch_spec:
392 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
393 if self.svn_branch:
394 break
395 tag_spec = RunGit(
396 ['config', 'svn-remote.%s.tags' % remote],
397 error_ok=True).strip()
398 if tag_spec:
399 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
400 if self.svn_branch:
401 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402
403 if not self.svn_branch:
404 DieWithError('Can\'t guess svn branch -- try specifying it on the '
405 'command line')
406
407 return self.svn_branch
408
409 def GetTreeStatusUrl(self, error_ok=False):
410 if not self.tree_status_url:
411 error_message = ('You must configure your tree status URL by running '
412 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000413 self.tree_status_url = self._GetRietveldConfig(
414 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 return self.tree_status_url
416
417 def GetViewVCUrl(self):
418 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000419 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000420 return self.viewvc_url
421
rmistry@google.com90752582014-01-14 21:04:50 +0000422 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000423 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000424
rmistry@google.com5626a922015-02-26 14:03:30 +0000425 def GetRunPostUploadHook(self):
426 run_post_upload_hook = self._GetRietveldConfig(
427 'run-post-upload-hook', error_ok=True)
428 return run_post_upload_hook == "True"
429
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000430 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000431 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000432
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000433 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000434 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000435
ukai@chromium.orge8077812012-02-03 03:41:46 +0000436 def GetIsGerrit(self):
437 """Return true if this repo is assosiated with gerrit code review system."""
438 if self.is_gerrit is None:
439 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
440 return self.is_gerrit
441
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000442 def GetGitEditor(self):
443 """Return the editor specified in the git config, or None if none is."""
444 if self.git_editor is None:
445 self.git_editor = self._GetConfig('core.editor', error_ok=True)
446 return self.git_editor or None
447
thestig@chromium.org44202a22014-03-11 19:22:18 +0000448 def GetLintRegex(self):
449 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
450 DEFAULT_LINT_REGEX)
451
452 def GetLintIgnoreRegex(self):
453 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
454 DEFAULT_LINT_IGNORE_REGEX)
455
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000456 def GetProject(self):
457 if not self.project:
458 self.project = self._GetRietveldConfig('project', error_ok=True)
459 return self.project
460
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000461 def GetForceHttpsCommitUrl(self):
462 if not self.force_https_commit_url:
463 self.force_https_commit_url = self._GetRietveldConfig(
464 'force-https-commit-url', error_ok=True)
465 return self.force_https_commit_url
466
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000467 def GetPendingRefPrefix(self):
468 if not self.pending_ref_prefix:
469 self.pending_ref_prefix = self._GetRietveldConfig(
470 'pending-ref-prefix', error_ok=True)
471 return self.pending_ref_prefix
472
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000473 def _GetRietveldConfig(self, param, **kwargs):
474 return self._GetConfig('rietveld.' + param, **kwargs)
475
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 def _GetConfig(self, param, **kwargs):
477 self.LazyUpdateIfNeeded()
478 return RunGit(['config', param], **kwargs).strip()
479
480
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481def ShortBranchName(branch):
482 """Convert a name like 'refs/heads/foo' to just 'foo'."""
483 return branch.replace('refs/heads/', '')
484
485
486class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000487 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000488 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000489 global settings
490 if not settings:
491 # Happens when git_cl.py is used as a utility library.
492 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 settings.GetDefaultServerUrl()
494 self.branchref = branchref
495 if self.branchref:
496 self.branch = ShortBranchName(self.branchref)
497 else:
498 self.branch = None
499 self.rietveld_server = None
500 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000501 self.lookedup_issue = False
502 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503 self.has_description = False
504 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000505 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000506 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000507 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000508 self.cc = None
509 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000510 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000511 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000512
513 def GetCCList(self):
514 """Return the users cc'd on this CL.
515
516 Return is a string suitable for passing to gcl with the --cc flag.
517 """
518 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000519 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000520 more_cc = ','.join(self.watchers)
521 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
522 return self.cc
523
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000524 def GetCCListWithoutDefault(self):
525 """Return the users cc'd on this CL excluding default ones."""
526 if self.cc is None:
527 self.cc = ','.join(self.watchers)
528 return self.cc
529
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000530 def SetWatchers(self, watchers):
531 """Set the list of email addresses that should be cc'd based on the changed
532 files in this CL.
533 """
534 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000535
536 def GetBranch(self):
537 """Returns the short branch name, e.g. 'master'."""
538 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000539 branchref = RunGit(['symbolic-ref', 'HEAD'],
540 stderr=subprocess2.VOID, error_ok=True).strip()
541 if not branchref:
542 return None
543 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000544 self.branch = ShortBranchName(self.branchref)
545 return self.branch
546
547 def GetBranchRef(self):
548 """Returns the full branch name, e.g. 'refs/heads/master'."""
549 self.GetBranch() # Poke the lazy loader.
550 return self.branchref
551
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000552 @staticmethod
553 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000554 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000555 e.g. 'origin', 'refs/heads/master'
556 """
557 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000558 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
559 error_ok=True).strip()
560 if upstream_branch:
561 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
562 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000563 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
564 error_ok=True).strip()
565 if upstream_branch:
566 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000567 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000568 # Fall back on trying a git-svn upstream branch.
569 if settings.GetIsGitSvn():
570 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000572 # Else, try to guess the origin remote.
573 remote_branches = RunGit(['branch', '-r']).split()
574 if 'origin/master' in remote_branches:
575 # Fall back on origin/master if it exits.
576 remote = 'origin'
577 upstream_branch = 'refs/heads/master'
578 elif 'origin/trunk' in remote_branches:
579 # Fall back on origin/trunk if it exists. Generally a shared
580 # git-svn clone
581 remote = 'origin'
582 upstream_branch = 'refs/heads/trunk'
583 else:
584 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585Either pass complete "git diff"-style arguments, like
586 git cl upload origin/master
587or verify this branch is set up to track another (via the --track argument to
588"git checkout -b ...").""")
589
590 return remote, upstream_branch
591
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000592 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000593 return git_common.get_or_create_merge_base(self.GetBranch(),
594 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000595
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596 def GetUpstreamBranch(self):
597 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000598 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000600 upstream_branch = upstream_branch.replace('refs/heads/',
601 'refs/remotes/%s/' % remote)
602 upstream_branch = upstream_branch.replace('refs/branch-heads/',
603 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 self.upstream_branch = upstream_branch
605 return self.upstream_branch
606
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000607 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000608 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000609 remote, branch = None, self.GetBranch()
610 seen_branches = set()
611 while branch not in seen_branches:
612 seen_branches.add(branch)
613 remote, branch = self.FetchUpstreamTuple(branch)
614 branch = ShortBranchName(branch)
615 if remote != '.' or branch.startswith('refs/remotes'):
616 break
617 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000618 remotes = RunGit(['remote'], error_ok=True).split()
619 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000620 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000621 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000622 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000623 logging.warning('Could not determine which remote this change is '
624 'associated with, so defaulting to "%s". This may '
625 'not be what you want. You may prevent this message '
626 'by running "git svn info" as documented here: %s',
627 self._remote,
628 GIT_INSTRUCTIONS_URL)
629 else:
630 logging.warn('Could not determine which remote this change is '
631 'associated with. You may prevent this message by '
632 'running "git svn info" as documented here: %s',
633 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000634 branch = 'HEAD'
635 if branch.startswith('refs/remotes'):
636 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000637 elif branch.startswith('refs/branch-heads/'):
638 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000639 else:
640 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000641 return self._remote
642
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000643 def GitSanityChecks(self, upstream_git_obj):
644 """Checks git repo status and ensures diff is from local commits."""
645
sbc@chromium.org79706062015-01-14 21:18:12 +0000646 if upstream_git_obj is None:
647 if self.GetBranch() is None:
648 print >> sys.stderr, (
649 'ERROR: unable to dertermine current branch (detached HEAD?)')
650 else:
651 print >> sys.stderr, (
652 'ERROR: no upstream branch')
653 return False
654
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000655 # Verify the commit we're diffing against is in our current branch.
656 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
657 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
658 if upstream_sha != common_ancestor:
659 print >> sys.stderr, (
660 'ERROR: %s is not in the current branch. You may need to rebase '
661 'your tracking branch' % upstream_sha)
662 return False
663
664 # List the commits inside the diff, and verify they are all local.
665 commits_in_diff = RunGit(
666 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
667 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
668 remote_branch = remote_branch.strip()
669 if code != 0:
670 _, remote_branch = self.GetRemoteBranch()
671
672 commits_in_remote = RunGit(
673 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
674
675 common_commits = set(commits_in_diff) & set(commits_in_remote)
676 if common_commits:
677 print >> sys.stderr, (
678 'ERROR: Your diff contains %d commits already in %s.\n'
679 'Run "git log --oneline %s..HEAD" to get a list of commits in '
680 'the diff. If you are using a custom git flow, you can override'
681 ' the reference used for this check with "git config '
682 'gitcl.remotebranch <git-ref>".' % (
683 len(common_commits), remote_branch, upstream_git_obj))
684 return False
685 return True
686
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000687 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000688 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000689
690 Returns None if it is not set.
691 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000692 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
693 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000694
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000695 def GetGitSvnRemoteUrl(self):
696 """Return the configured git-svn remote URL parsed from git svn info.
697
698 Returns None if it is not set.
699 """
700 # URL is dependent on the current directory.
701 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
702 if data:
703 keys = dict(line.split(': ', 1) for line in data.splitlines()
704 if ': ' in line)
705 return keys.get('URL', None)
706 return None
707
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 def GetRemoteUrl(self):
709 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
710
711 Returns None if there is no remote.
712 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000713 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000714 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
715
716 # If URL is pointing to a local directory, it is probably a git cache.
717 if os.path.isdir(url):
718 url = RunGit(['config', 'remote.%s.url' % remote],
719 error_ok=True,
720 cwd=url).strip()
721 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722
723 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000724 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000725 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000727 self.issue = int(issue) or None if issue else None
728 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 return self.issue
730
731 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000732 if not self.rietveld_server:
733 # If we're on a branch then get the server potentially associated
734 # with that branch.
735 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000736 rietveld_server_config = self._RietveldServer()
737 if rietveld_server_config:
738 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
739 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000740 if not self.rietveld_server:
741 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742 return self.rietveld_server
743
744 def GetIssueURL(self):
745 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000746 if not self.GetIssue():
747 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
749
750 def GetDescription(self, pretty=False):
751 if not self.has_description:
752 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000753 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000754 try:
755 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000756 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000757 if e.code == 404:
758 DieWithError(
759 ('\nWhile fetching the description for issue %d, received a '
760 '404 (not found)\n'
761 'error. It is likely that you deleted this '
762 'issue on the server. If this is the\n'
763 'case, please run\n\n'
764 ' git cl issue 0\n\n'
765 'to clear the association with the deleted issue. Then run '
766 'this command again.') % issue)
767 else:
768 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000769 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000770 except urllib2.URLError as e:
771 print >> sys.stderr, (
772 'Warning: Failed to retrieve CL description due to network '
773 'failure.')
774 self.description = ''
775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 self.has_description = True
777 if pretty:
778 wrapper = textwrap.TextWrapper()
779 wrapper.initial_indent = wrapper.subsequent_indent = ' '
780 return wrapper.fill(self.description)
781 return self.description
782
783 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000784 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000785 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 patchset = RunGit(['config', self._PatchsetSetting()],
787 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000788 self.patchset = int(patchset) or None if patchset else None
789 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 return self.patchset
791
792 def SetPatchset(self, patchset):
793 """Set this branch's patchset. If patchset=0, clears the patchset."""
794 if patchset:
795 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000796 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 else:
798 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000799 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000800 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000802 def GetMostRecentPatchset(self):
803 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000804
805 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000806 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000807 '/download/issue%s_%s.diff' % (issue, patchset))
808
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000809 def GetIssueProperties(self):
810 if self._props is None:
811 issue = self.GetIssue()
812 if not issue:
813 self._props = {}
814 else:
815 self._props = self.RpcServer().get_issue_properties(issue, True)
816 return self._props
817
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000818 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000819 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000820
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000821 def AddComment(self, message):
822 return self.RpcServer().add_comment(self.GetIssue(), message)
823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 def SetIssue(self, issue):
825 """Set this branch's issue. If issue=0, clears the issue."""
826 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000827 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 RunGit(['config', self._IssueSetting(), str(issue)])
829 if self.rietveld_server:
830 RunGit(['config', self._RietveldServer(), self.rietveld_server])
831 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000832 current_issue = self.GetIssue()
833 if current_issue:
834 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000835 self.issue = None
836 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000838 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000839 if not self.GitSanityChecks(upstream_branch):
840 DieWithError('\nGit sanity check failure')
841
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000843 if not root:
844 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000845 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000846
847 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000848 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000849 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000850 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000851 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000852 except subprocess2.CalledProcessError:
853 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000854 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000855 'This branch probably doesn\'t exist anymore. To reset the\n'
856 'tracking branch, please run\n'
857 ' git branch --set-upstream %s trunk\n'
858 'replacing trunk with origin/master or the relevant branch') %
859 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000860
maruel@chromium.org52424302012-08-29 15:14:30 +0000861 issue = self.GetIssue()
862 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000863 if issue:
864 description = self.GetDescription()
865 else:
866 # If the change was never uploaded, use the log messages of all commits
867 # up to the branch point, as git cl upload will prefill the description
868 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000869 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
870 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000871
872 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000873 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000874 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000875 name,
876 description,
877 absroot,
878 files,
879 issue,
880 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000881 author,
882 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000883
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000884 def GetStatus(self):
885 """Apply a rough heuristic to give a simple summary of an issue's review
886 or CQ status, assuming adherence to a common workflow.
887
888 Returns None if no issue for this branch, or one of the following keywords:
889 * 'error' - error from review tool (including deleted issues)
890 * 'unsent' - not sent for review
891 * 'waiting' - waiting for review
892 * 'reply' - waiting for owner to reply to review
893 * 'lgtm' - LGTM from at least one approved reviewer
894 * 'commit' - in the commit queue
895 * 'closed' - closed
896 """
897 if not self.GetIssue():
898 return None
899
900 try:
901 props = self.GetIssueProperties()
902 except urllib2.HTTPError:
903 return 'error'
904
905 if props.get('closed'):
906 # Issue is closed.
907 return 'closed'
908 if props.get('commit'):
909 # Issue is in the commit queue.
910 return 'commit'
911
912 try:
913 reviewers = self.GetApprovingReviewers()
914 except urllib2.HTTPError:
915 return 'error'
916
917 if reviewers:
918 # Was LGTM'ed.
919 return 'lgtm'
920
921 messages = props.get('messages') or []
922
923 if not messages:
924 # No message was sent.
925 return 'unsent'
926 if messages[-1]['sender'] != props.get('owner_email'):
927 # Non-LGTM reply from non-owner
928 return 'reply'
929 return 'waiting'
930
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000931 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000932 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000933
934 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000935 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000936 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000937 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000938 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000939 except presubmit_support.PresubmitFailure, e:
940 DieWithError(
941 ('%s\nMaybe your depot_tools is out of date?\n'
942 'If all fails, contact maruel@') % e)
943
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000944 def UpdateDescription(self, description):
945 self.description = description
946 return self.RpcServer().update_description(
947 self.GetIssue(), self.description)
948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000949 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000950 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000951 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000953 def SetFlag(self, flag, value):
954 """Patchset must match."""
955 if not self.GetPatchset():
956 DieWithError('The patchset needs to match. Send another patchset.')
957 try:
958 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000959 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000960 except urllib2.HTTPError, e:
961 if e.code == 404:
962 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
963 if e.code == 403:
964 DieWithError(
965 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
966 'match?') % (self.GetIssue(), self.GetPatchset()))
967 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000969 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 """Returns an upload.RpcServer() to access this review's rietveld instance.
971 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000972 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000973 self._rpc_server = rietveld.CachingRietveld(
974 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000975 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976
977 def _IssueSetting(self):
978 """Return the git setting that stores this change's issue."""
979 return 'branch.%s.rietveldissue' % self.GetBranch()
980
981 def _PatchsetSetting(self):
982 """Return the git setting that stores this change's most recent patchset."""
983 return 'branch.%s.rietveldpatchset' % self.GetBranch()
984
985 def _RietveldServer(self):
986 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000987 branch = self.GetBranch()
988 if branch:
989 return 'branch.%s.rietveldserver' % branch
990 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991
992
993def GetCodereviewSettingsInteractively():
994 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000995 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 server = settings.GetDefaultServerUrl(error_ok=True)
997 prompt = 'Rietveld server (host[:port])'
998 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000999 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 if not server and not newserver:
1001 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001002 if newserver:
1003 newserver = gclient_utils.UpgradeToHttps(newserver)
1004 if newserver != server:
1005 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001007 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 prompt = caption
1009 if initial:
1010 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001011 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012 if new_val == 'x':
1013 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001014 elif new_val:
1015 if is_url:
1016 new_val = gclient_utils.UpgradeToHttps(new_val)
1017 if new_val != initial:
1018 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001020 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001021 SetProperty(settings.GetDefaultPrivateFlag(),
1022 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001024 'tree-status-url', False)
1025 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001026 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001027 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1028 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029
1030 # TODO: configure a default branch to diff against, rather than this
1031 # svn-based hackery.
1032
1033
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001034class ChangeDescription(object):
1035 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001036 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001037 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001038
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001039 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001040 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001041
agable@chromium.org42c20792013-09-12 17:34:49 +00001042 @property # www.logilab.org/ticket/89786
1043 def description(self): # pylint: disable=E0202
1044 return '\n'.join(self._description_lines)
1045
1046 def set_description(self, desc):
1047 if isinstance(desc, basestring):
1048 lines = desc.splitlines()
1049 else:
1050 lines = [line.rstrip() for line in desc]
1051 while lines and not lines[0]:
1052 lines.pop(0)
1053 while lines and not lines[-1]:
1054 lines.pop(-1)
1055 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001056
piman@chromium.org336f9122014-09-04 02:16:55 +00001057 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001058 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001059 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001060 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001061 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001062 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001063
agable@chromium.org42c20792013-09-12 17:34:49 +00001064 # Get the set of R= and TBR= lines and remove them from the desciption.
1065 regexp = re.compile(self.R_LINE)
1066 matches = [regexp.match(line) for line in self._description_lines]
1067 new_desc = [l for i, l in enumerate(self._description_lines)
1068 if not matches[i]]
1069 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001070
agable@chromium.org42c20792013-09-12 17:34:49 +00001071 # Construct new unified R= and TBR= lines.
1072 r_names = []
1073 tbr_names = []
1074 for match in matches:
1075 if not match:
1076 continue
1077 people = cleanup_list([match.group(2).strip()])
1078 if match.group(1) == 'TBR':
1079 tbr_names.extend(people)
1080 else:
1081 r_names.extend(people)
1082 for name in r_names:
1083 if name not in reviewers:
1084 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001085 if add_owners_tbr:
1086 owners_db = owners.Database(change.RepositoryRoot(),
1087 fopen=file, os_path=os.path, glob=glob.glob)
1088 all_reviewers = set(tbr_names + reviewers)
1089 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1090 all_reviewers)
1091 tbr_names.extend(owners_db.reviewers_for(missing_files,
1092 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001093 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1094 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1095
1096 # Put the new lines in the description where the old first R= line was.
1097 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1098 if 0 <= line_loc < len(self._description_lines):
1099 if new_tbr_line:
1100 self._description_lines.insert(line_loc, new_tbr_line)
1101 if new_r_line:
1102 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001103 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001104 if new_r_line:
1105 self.append_footer(new_r_line)
1106 if new_tbr_line:
1107 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001108
1109 def prompt(self):
1110 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001111 self.set_description([
1112 '# Enter a description of the change.',
1113 '# This will be displayed on the codereview site.',
1114 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001115 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001116 '--------------------',
1117 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001118
agable@chromium.org42c20792013-09-12 17:34:49 +00001119 regexp = re.compile(self.BUG_LINE)
1120 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001121 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001122 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001123 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001124 if not content:
1125 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001126 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001127
1128 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001129 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1130 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001131 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001132 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001133
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001134 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001135 if self._description_lines:
1136 # Add an empty line if either the last line or the new line isn't a tag.
1137 last_line = self._description_lines[-1]
1138 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1139 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1140 self._description_lines.append('')
1141 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001142
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001143 def get_reviewers(self):
1144 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001145 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1146 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001147 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001148
1149
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001150def get_approving_reviewers(props):
1151 """Retrieves the reviewers that approved a CL from the issue properties with
1152 messages.
1153
1154 Note that the list may contain reviewers that are not committer, thus are not
1155 considered by the CQ.
1156 """
1157 return sorted(
1158 set(
1159 message['sender']
1160 for message in props['messages']
1161 if message['approval'] and message['sender'] in props['reviewers']
1162 )
1163 )
1164
1165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166def FindCodereviewSettingsFile(filename='codereview.settings'):
1167 """Finds the given file starting in the cwd and going up.
1168
1169 Only looks up to the top of the repository unless an
1170 'inherit-review-settings-ok' file exists in the root of the repository.
1171 """
1172 inherit_ok_file = 'inherit-review-settings-ok'
1173 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001174 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1176 root = '/'
1177 while True:
1178 if filename in os.listdir(cwd):
1179 if os.path.isfile(os.path.join(cwd, filename)):
1180 return open(os.path.join(cwd, filename))
1181 if cwd == root:
1182 break
1183 cwd = os.path.dirname(cwd)
1184
1185
1186def LoadCodereviewSettingsFromFile(fileobj):
1187 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001188 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 def SetProperty(name, setting, unset_error_ok=False):
1191 fullname = 'rietveld.' + name
1192 if setting in keyvals:
1193 RunGit(['config', fullname, keyvals[setting]])
1194 else:
1195 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1196
1197 SetProperty('server', 'CODE_REVIEW_SERVER')
1198 # Only server setting is required. Other settings can be absent.
1199 # In that case, we ignore errors raised during option deletion attempt.
1200 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001201 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1203 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001204 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001205 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001206 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1207 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001208 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001209 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001210 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001211 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1212 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001214 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001215 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1218 #should be of the form
1219 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1220 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1221 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1222 keyvals['ORIGIN_URL_CONFIG']])
1223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001225def urlretrieve(source, destination):
1226 """urllib is broken for SSL connections via a proxy therefore we
1227 can't use urllib.urlretrieve()."""
1228 with open(destination, 'w') as f:
1229 f.write(urllib2.urlopen(source).read())
1230
1231
ukai@chromium.org712d6102013-11-27 00:52:58 +00001232def hasSheBang(fname):
1233 """Checks fname is a #! script."""
1234 with open(fname) as f:
1235 return f.read(2).startswith('#!')
1236
1237
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001238def DownloadHooks(force):
1239 """downloads hooks
1240
1241 Args:
1242 force: True to update hooks. False to install hooks if not present.
1243 """
1244 if not settings.GetIsGerrit():
1245 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001246 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001247 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1248 if not os.access(dst, os.X_OK):
1249 if os.path.exists(dst):
1250 if not force:
1251 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001252 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001253 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001254 if not hasSheBang(dst):
1255 DieWithError('Not a script: %s\n'
1256 'You need to download from\n%s\n'
1257 'into .git/hooks/commit-msg and '
1258 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001259 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1260 except Exception:
1261 if os.path.exists(dst):
1262 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001263 DieWithError('\nFailed to download hooks.\n'
1264 'You need to download from\n%s\n'
1265 'into .git/hooks/commit-msg and '
1266 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001267
1268
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001269@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001271 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001273 parser.add_option('--activate-update', action='store_true',
1274 help='activate auto-updating [rietveld] section in '
1275 '.git/config')
1276 parser.add_option('--deactivate-update', action='store_true',
1277 help='deactivate auto-updating [rietveld] section in '
1278 '.git/config')
1279 options, args = parser.parse_args(args)
1280
1281 if options.deactivate_update:
1282 RunGit(['config', 'rietveld.autoupdate', 'false'])
1283 return
1284
1285 if options.activate_update:
1286 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1287 return
1288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289 if len(args) == 0:
1290 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001291 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292 return 0
1293
1294 url = args[0]
1295 if not url.endswith('codereview.settings'):
1296 url = os.path.join(url, 'codereview.settings')
1297
1298 # Load code review settings and download hooks (if available).
1299 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001300 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 return 0
1302
1303
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001304def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001305 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001306 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1307 branch = ShortBranchName(branchref)
1308 _, args = parser.parse_args(args)
1309 if not args:
1310 print("Current base-url:")
1311 return RunGit(['config', 'branch.%s.base-url' % branch],
1312 error_ok=False).strip()
1313 else:
1314 print("Setting base-url to %s" % args[0])
1315 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1316 error_ok=False).strip()
1317
1318
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001319def color_for_status(status):
1320 """Maps a Changelist status to color, for CMDstatus and other tools."""
1321 return {
1322 'unsent': Fore.RED,
1323 'waiting': Fore.BLUE,
1324 'reply': Fore.YELLOW,
1325 'lgtm': Fore.GREEN,
1326 'commit': Fore.MAGENTA,
1327 'closed': Fore.CYAN,
1328 'error': Fore.WHITE,
1329 }.get(status, Fore.WHITE)
1330
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001331def fetch_cl_status(b):
1332 """Fetches information for an issue and returns (branch, issue, color)."""
1333 c = Changelist(branchref=b)
1334 i = c.GetIssueURL()
1335 status = c.GetStatus()
1336 color = color_for_status(status)
1337
1338 if i and (not status or status == 'error'):
1339 # The issue probably doesn't exist anymore.
1340 i += ' (broken)'
1341
1342 return (b, i, color)
1343
1344def get_cl_statuses(branches, fine_grained, max_processes=None):
1345 """Returns a blocking iterable of (branch, issue, color) for given branches.
1346
1347 If fine_grained is true, this will fetch CL statuses from the server.
1348 Otherwise, simply indicate if there's a matching url for the given branches.
1349
1350 If max_processes is specified, it is used as the maximum number of processes
1351 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1352 spawned.
1353 """
1354 # Silence upload.py otherwise it becomes unwieldly.
1355 upload.verbosity = 0
1356
1357 if fine_grained:
1358 # Process one branch synchronously to work through authentication, then
1359 # spawn processes to process all the other branches in parallel.
1360 if branches:
1361 yield fetch_cl_status(branches[0])
1362
1363 branches_to_fetch = branches[1:]
1364 pool = ThreadPool(
1365 min(max_processes, len(branches_to_fetch))
1366 if max_processes is not None
1367 else len(branches_to_fetch))
1368 for x in pool.imap_unordered(fetch_cl_status, branches_to_fetch):
1369 yield x
1370 else:
1371 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1372 for b in branches:
1373 c = Changelist(branchref=b)
1374 url = c.GetIssueURL()
1375 yield (b, url, Fore.BLUE if url else Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001378 """Show status of changelists.
1379
1380 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001381 - Red not sent for review or broken
1382 - Blue waiting for review
1383 - Yellow waiting for you to reply to review
1384 - Green LGTM'ed
1385 - Magenta in the commit queue
1386 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001387
1388 Also see 'git cl comments'.
1389 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 parser.add_option('--field',
1391 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001392 parser.add_option('-f', '--fast', action='store_true',
1393 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001394 parser.add_option(
1395 '-j', '--maxjobs', action='store', type=int,
1396 help='The maximum number of jobs to use when retrieving review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001398 if args:
1399 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001402 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 if options.field.startswith('desc'):
1404 print cl.GetDescription()
1405 elif options.field == 'id':
1406 issueid = cl.GetIssue()
1407 if issueid:
1408 print issueid
1409 elif options.field == 'patch':
1410 patchset = cl.GetPatchset()
1411 if patchset:
1412 print patchset
1413 elif options.field == 'url':
1414 url = cl.GetIssueURL()
1415 if url:
1416 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001417 return 0
1418
1419 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1420 if not branches:
1421 print('No local branch found.')
1422 return 0
1423
1424 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001425 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001426 alignment = max(5, max(len(b) for b in branches))
1427 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001428 output = get_cl_statuses(branches,
1429 fine_grained=not options.fast,
1430 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001431
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001432 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001433 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001434 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001435 while branch not in branch_statuses:
1436 b, i, color = output.next()
1437 branch_statuses[b] = (i, color)
1438 issue, color = branch_statuses.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001439 reset = Fore.RESET
1440 if not sys.stdout.isatty():
1441 color = ''
1442 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001443 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001444 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001446 cl = Changelist()
1447 print
1448 print 'Current branch:',
1449 if not cl.GetIssue():
1450 print 'no issue assigned.'
1451 return 0
1452 print cl.GetBranch()
1453 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001454 if not options.fast:
1455 print 'Issue description:'
1456 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 return 0
1458
1459
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001460def colorize_CMDstatus_doc():
1461 """To be called once in main() to add colors to git cl status help."""
1462 colors = [i for i in dir(Fore) if i[0].isupper()]
1463
1464 def colorize_line(line):
1465 for color in colors:
1466 if color in line.upper():
1467 # Extract whitespaces first and the leading '-'.
1468 indent = len(line) - len(line.lstrip(' ')) + 1
1469 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1470 return line
1471
1472 lines = CMDstatus.__doc__.splitlines()
1473 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1474
1475
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001476@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001478 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479
1480 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001481 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001482 parser.add_option('-r', '--reverse', action='store_true',
1483 help='Lookup the branch(es) for the specified issues. If '
1484 'no issues are specified, all branches with mapped '
1485 'issues will be listed.')
1486 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487
dnj@chromium.org406c4402015-03-03 17:22:28 +00001488 if options.reverse:
1489 branches = RunGit(['for-each-ref', 'refs/heads',
1490 '--format=%(refname:short)']).splitlines()
1491
1492 # Reverse issue lookup.
1493 issue_branch_map = {}
1494 for branch in branches:
1495 cl = Changelist(branchref=branch)
1496 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1497 if not args:
1498 args = sorted(issue_branch_map.iterkeys())
1499 for issue in args:
1500 if not issue:
1501 continue
1502 print 'Branch for issue number %s: %s' % (
1503 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1504 else:
1505 cl = Changelist()
1506 if len(args) > 0:
1507 try:
1508 issue = int(args[0])
1509 except ValueError:
1510 DieWithError('Pass a number to set the issue or none to list it.\n'
1511 'Maybe you want to run git cl status?')
1512 cl.SetIssue(issue)
1513 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001514 return 0
1515
1516
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001517def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001518 """Shows or posts review comments for any changelist."""
1519 parser.add_option('-a', '--add-comment', dest='comment',
1520 help='comment to add to an issue')
1521 parser.add_option('-i', dest='issue',
1522 help="review issue id (defaults to current issue)")
1523 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001524
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001525 issue = None
1526 if options.issue:
1527 try:
1528 issue = int(options.issue)
1529 except ValueError:
1530 DieWithError('A review issue id is expected to be a number')
1531
1532 cl = Changelist(issue=issue)
1533
1534 if options.comment:
1535 cl.AddComment(options.comment)
1536 return 0
1537
1538 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001539 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001540 if message['disapproval']:
1541 color = Fore.RED
1542 elif message['approval']:
1543 color = Fore.GREEN
1544 elif message['sender'] == data['owner_email']:
1545 color = Fore.MAGENTA
1546 else:
1547 color = Fore.BLUE
1548 print '\n%s%s %s%s' % (
1549 color, message['date'].split('.', 1)[0], message['sender'],
1550 Fore.RESET)
1551 if message['text'].strip():
1552 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001553 return 0
1554
1555
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001556def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001557 """Brings up the editor for the current CL's description."""
wychen@chromium.org16ca29c2015-04-02 22:26:55 +00001558 parser.parse_args(args)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001559 cl = Changelist()
1560 if not cl.GetIssue():
1561 DieWithError('This branch has no associated changelist.')
1562 description = ChangeDescription(cl.GetDescription())
1563 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001564 if cl.GetDescription() != description.description:
1565 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001566 return 0
1567
1568
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001569def CreateDescriptionFromLog(args):
1570 """Pulls out the commit log to use as a base for the CL description."""
1571 log_args = []
1572 if len(args) == 1 and not args[0].endswith('.'):
1573 log_args = [args[0] + '..']
1574 elif len(args) == 1 and args[0].endswith('...'):
1575 log_args = [args[0][:-1]]
1576 elif len(args) == 2:
1577 log_args = [args[0] + '..' + args[1]]
1578 else:
1579 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001580 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001581
1582
thestig@chromium.org44202a22014-03-11 19:22:18 +00001583def CMDlint(parser, args):
1584 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001585 parser.add_option('--filter', action='append', metavar='-x,+y',
1586 help='Comma-separated list of cpplint\'s category-filters')
1587 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001588
1589 # Access to a protected member _XX of a client class
1590 # pylint: disable=W0212
1591 try:
1592 import cpplint
1593 import cpplint_chromium
1594 except ImportError:
1595 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1596 return 1
1597
1598 # Change the current working directory before calling lint so that it
1599 # shows the correct base.
1600 previous_cwd = os.getcwd()
1601 os.chdir(settings.GetRoot())
1602 try:
1603 cl = Changelist()
1604 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1605 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001606 if not files:
1607 print "Cannot lint an empty CL"
1608 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001609
1610 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001611 command = args + files
1612 if options.filter:
1613 command = ['--filter=' + ','.join(options.filter)] + command
1614 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001615
1616 white_regex = re.compile(settings.GetLintRegex())
1617 black_regex = re.compile(settings.GetLintIgnoreRegex())
1618 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1619 for filename in filenames:
1620 if white_regex.match(filename):
1621 if black_regex.match(filename):
1622 print "Ignoring file %s" % filename
1623 else:
1624 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1625 extra_check_functions)
1626 else:
1627 print "Skipping file %s" % filename
1628 finally:
1629 os.chdir(previous_cwd)
1630 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1631 if cpplint._cpplint_state.error_count != 0:
1632 return 1
1633 return 0
1634
1635
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001636def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001637 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001638 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001640 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001641 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 (options, args) = parser.parse_args(args)
1643
sbc@chromium.org71437c02015-04-09 19:29:40 +00001644 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001645 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646 return 1
1647
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001648 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 if args:
1650 base_branch = args[0]
1651 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001652 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001653 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001655 cl.RunHook(
1656 committing=not options.upload,
1657 may_prompt=False,
1658 verbose=options.verbose,
1659 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001660 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001661
1662
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001663def AddChangeIdToCommitMessage(options, args):
1664 """Re-commits using the current message, assumes the commit hook is in
1665 place.
1666 """
1667 log_desc = options.message or CreateDescriptionFromLog(args)
1668 git_command = ['commit', '--amend', '-m', log_desc]
1669 RunGit(git_command)
1670 new_log_desc = CreateDescriptionFromLog(args)
1671 if CHANGE_ID in new_log_desc:
1672 print 'git-cl: Added Change-Id to commit message.'
1673 else:
1674 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1675
1676
piman@chromium.org336f9122014-09-04 02:16:55 +00001677def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001678 """upload the current branch to gerrit."""
1679 # We assume the remote called "origin" is the one we want.
1680 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001681 gerrit_remote = 'origin'
ukai@chromium.orge8077812012-02-03 03:41:46 +00001682 branch = 'master'
1683 if options.target_branch:
1684 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001685
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001686 change_desc = ChangeDescription(
1687 options.message or CreateDescriptionFromLog(args))
1688 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001689 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001691
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001692 if options.squash:
1693 # Try to get the message from a previous upload.
1694 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1695 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1696 if not message:
1697 if not options.force:
1698 change_desc.prompt()
1699
1700 if CHANGE_ID not in change_desc.description:
1701 # Run the commit-msg hook without modifying the head commit by writing
1702 # the commit message to a temporary file and running the hook over it,
1703 # then reading the file back in.
1704 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1705 'commit-msg')
1706 file_handle, msg_file = tempfile.mkstemp(text=True,
1707 prefix='commit_msg')
1708 try:
1709 try:
1710 with os.fdopen(file_handle, 'w') as fileobj:
1711 fileobj.write(change_desc.description)
1712 finally:
1713 os.close(file_handle)
1714 RunCommand([commit_msg_hook, msg_file])
1715 change_desc.set_description(gclient_utils.FileRead(msg_file))
1716 finally:
1717 os.remove(msg_file)
1718
1719 if not change_desc.description:
1720 print "Description is empty; aborting."
1721 return 1
1722
1723 message = change_desc.description
1724
1725 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1726 if remote is '.':
1727 # If our upstream branch is local, we base our squashed commit on its
1728 # squashed version.
1729 parent = ('refs/heads/git_cl_uploads/' +
1730 scm.GIT.ShortBranchName(upstream_branch))
1731
1732 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1733 # will create additional CLs when uploading.
1734 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1735 RunGitSilent(['rev-parse', parent + ':'])):
1736 print 'Upload upstream branch ' + upstream_branch + ' first.'
1737 return 1
1738 else:
1739 parent = cl.GetCommonAncestorWithUpstream()
1740
1741 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1742 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1743 '-m', message]).strip()
1744 else:
1745 if CHANGE_ID not in change_desc.description:
1746 AddChangeIdToCommitMessage(options, args)
1747 ref_to_push = 'HEAD'
1748 parent = '%s/%s' % (gerrit_remote, branch)
1749
1750 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1751 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001752 if len(commits) > 1:
1753 print('WARNING: This will upload %d commits. Run the following command '
1754 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001755 print('git log %s..%s' % (parent, ref_to_push))
1756 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001757 'commit.')
1758 ask_for_data('About to upload; enter to confirm.')
1759
piman@chromium.org336f9122014-09-04 02:16:55 +00001760 if options.reviewers or options.tbr_owners:
1761 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762
ukai@chromium.orge8077812012-02-03 03:41:46 +00001763 receive_options = []
1764 cc = cl.GetCCList().split(',')
1765 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001766 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001767 cc = filter(None, cc)
1768 if cc:
1769 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001770 if change_desc.get_reviewers():
1771 receive_options.extend(
1772 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773
ukai@chromium.orge8077812012-02-03 03:41:46 +00001774 git_command = ['push']
1775 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001776 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001777 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001778 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001779 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001780
1781 if options.squash:
1782 head = RunGit(['rev-parse', 'HEAD']).strip()
1783 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1784
ukai@chromium.orge8077812012-02-03 03:41:46 +00001785 # TODO(ukai): parse Change-Id: and set issue number?
1786 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788
wittman@chromium.org455dc922015-01-26 20:15:50 +00001789def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1790 """Computes the remote branch ref to use for the CL.
1791
1792 Args:
1793 remote (str): The git remote for the CL.
1794 remote_branch (str): The git remote branch for the CL.
1795 target_branch (str): The target branch specified by the user.
1796 pending_prefix (str): The pending prefix from the settings.
1797 """
1798 if not (remote and remote_branch):
1799 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001800
wittman@chromium.org455dc922015-01-26 20:15:50 +00001801 if target_branch:
1802 # Cannonicalize branch references to the equivalent local full symbolic
1803 # refs, which are then translated into the remote full symbolic refs
1804 # below.
1805 if '/' not in target_branch:
1806 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1807 else:
1808 prefix_replacements = (
1809 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1810 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1811 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1812 )
1813 match = None
1814 for regex, replacement in prefix_replacements:
1815 match = re.search(regex, target_branch)
1816 if match:
1817 remote_branch = target_branch.replace(match.group(0), replacement)
1818 break
1819 if not match:
1820 # This is a branch path but not one we recognize; use as-is.
1821 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00001822 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
1823 # Handle the refs that need to land in different refs.
1824 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001825
wittman@chromium.org455dc922015-01-26 20:15:50 +00001826 # Create the true path to the remote branch.
1827 # Does the following translation:
1828 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1829 # * refs/remotes/origin/master -> refs/heads/master
1830 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1831 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1832 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1833 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1834 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1835 'refs/heads/')
1836 elif remote_branch.startswith('refs/remotes/branch-heads'):
1837 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1838 # If a pending prefix exists then replace refs/ with it.
1839 if pending_prefix:
1840 remote_branch = remote_branch.replace('refs/', pending_prefix)
1841 return remote_branch
1842
1843
piman@chromium.org336f9122014-09-04 02:16:55 +00001844def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001845 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1847 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001848 if options.emulate_svn_auto_props:
1849 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001850
1851 change_desc = None
1852
pgervais@chromium.org91141372014-01-09 23:27:20 +00001853 if options.email is not None:
1854 upload_args.extend(['--email', options.email])
1855
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001856 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001857 if options.title:
1858 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001859 if options.message:
1860 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001861 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001862 print ("This branch is associated with issue %s. "
1863 "Adding patch to that issue." % cl.GetIssue())
1864 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001865 if options.title:
1866 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001867 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001868 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001869 if options.reviewers or options.tbr_owners:
1870 change_desc.update_reviewers(options.reviewers,
1871 options.tbr_owners,
1872 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001873 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001874 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001875
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001876 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001877 print "Description is empty; aborting."
1878 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001879
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001880 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001881 if change_desc.get_reviewers():
1882 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001883 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001884 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001885 DieWithError("Must specify reviewers to send email.")
1886 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001887
1888 # We check this before applying rietveld.private assuming that in
1889 # rietveld.cc only addresses which we can send private CLs to are listed
1890 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1891 # --private is specified explicitly on the command line.
1892 if options.private:
1893 logging.warn('rietveld.cc is ignored since private flag is specified. '
1894 'You need to review and add them manually if necessary.')
1895 cc = cl.GetCCListWithoutDefault()
1896 else:
1897 cc = cl.GetCCList()
1898 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001899 if cc:
1900 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001902 if options.private or settings.GetDefaultPrivateFlag() == "True":
1903 upload_args.append('--private')
1904
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001905 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001906 if not options.find_copies:
1907 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001908
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 # Include the upstream repo's URL in the change -- this is useful for
1910 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001911 remote_url = cl.GetGitBaseUrlFromConfig()
1912 if not remote_url:
1913 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001914 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001915 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001916 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1917 remote_url = (cl.GetRemoteUrl() + '@'
1918 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919 if remote_url:
1920 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00001921 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00001922 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1923 settings.GetPendingRefPrefix())
1924 if target_ref:
1925 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001927 project = settings.GetProject()
1928 if project:
1929 upload_args.extend(['--project', project])
1930
rmistry@google.comef966222015-04-07 11:15:01 +00001931 if options.cq_dry_run:
1932 upload_args.extend(['--cq_dry_run'])
1933
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001935 upload_args = ['upload'] + upload_args + args
1936 logging.info('upload.RealMain(%s)', upload_args)
1937 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001938 issue = int(issue)
1939 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001940 except KeyboardInterrupt:
1941 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942 except:
1943 # If we got an exception after the user typed a description for their
1944 # change, back up the description before re-raising.
1945 if change_desc:
1946 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1947 print '\nGot exception while uploading -- saving description to %s\n' \
1948 % backup_path
1949 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001950 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951 backup_file.close()
1952 raise
1953
1954 if not cl.GetIssue():
1955 cl.SetIssue(issue)
1956 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001957
1958 if options.use_commit_queue:
1959 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001960 return 0
1961
1962
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001963def cleanup_list(l):
1964 """Fixes a list so that comma separated items are put as individual items.
1965
1966 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1967 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1968 """
1969 items = sum((i.split(',') for i in l), [])
1970 stripped_items = (i.strip() for i in items)
1971 return sorted(filter(None, stripped_items))
1972
1973
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001974@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001975def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001976 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001977 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1978 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001979 parser.add_option('--bypass-watchlists', action='store_true',
1980 dest='bypass_watchlists',
1981 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001982 parser.add_option('-f', action='store_true', dest='force',
1983 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001984 parser.add_option('-m', dest='message', help='message for patchset')
1985 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001986 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001987 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001988 help='reviewer email addresses')
1989 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001990 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001991 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001992 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001993 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001994 parser.add_option('--emulate_svn_auto_props',
1995 '--emulate-svn-auto-props',
1996 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001997 dest="emulate_svn_auto_props",
1998 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001999 parser.add_option('-c', '--use-commit-queue', action='store_true',
2000 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002001 parser.add_option('--private', action='store_true',
2002 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002003 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002004 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002005 metavar='TARGET',
2006 help='Apply CL to remote ref TARGET. ' +
2007 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002008 parser.add_option('--squash', action='store_true',
2009 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002010 parser.add_option('--email', default=None,
2011 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002012 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2013 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002014 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2015 help='Send the patchset to do a CQ dry run right after '
2016 'upload.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002017
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002018 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002019 (options, args) = parser.parse_args(args)
2020
sbc@chromium.org71437c02015-04-09 19:29:40 +00002021 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002022 return 1
2023
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002024 options.reviewers = cleanup_list(options.reviewers)
2025 options.cc = cleanup_list(options.cc)
2026
ukai@chromium.orge8077812012-02-03 03:41:46 +00002027 cl = Changelist()
2028 if args:
2029 # TODO(ukai): is it ok for gerrit case?
2030 base_branch = args[0]
2031 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002032 if cl.GetBranch() is None:
2033 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2034
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002035 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002036 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002037 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002038
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002039 # Apply watchlists on upload.
2040 change = cl.GetChange(base_branch, None)
2041 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2042 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002043 if not options.bypass_watchlists:
2044 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002045
ukai@chromium.orge8077812012-02-03 03:41:46 +00002046 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002047 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002048 # Set the reviewer list now so that presubmit checks can access it.
2049 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002050 change_description.update_reviewers(options.reviewers,
2051 options.tbr_owners,
2052 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002053 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002054 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002055 may_prompt=not options.force,
2056 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002057 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002058 if not hook_results.should_continue():
2059 return 1
2060 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002061 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002062
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002063 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002064 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002065 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002066 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002067 print ('The last upload made from this repository was patchset #%d but '
2068 'the most recent patchset on the server is #%d.'
2069 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002070 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2071 'from another machine or branch the patch you\'re uploading now '
2072 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002073 ask_for_data('About to upload; enter to confirm.')
2074
iannucci@chromium.org79540052012-10-19 23:15:26 +00002075 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002076 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002077 return GerritUpload(options, args, cl, change)
2078 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002079 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002080 git_set_branch_value('last-upload-hash',
2081 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002082 # Run post upload hooks, if specified.
2083 if settings.GetRunPostUploadHook():
2084 presubmit_support.DoPostUploadExecuter(
2085 change,
2086 cl,
2087 settings.GetRoot(),
2088 options.verbose,
2089 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002090
2091 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002092
2093
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002094def IsSubmoduleMergeCommit(ref):
2095 # When submodules are added to the repo, we expect there to be a single
2096 # non-git-svn merge commit at remote HEAD with a signature comment.
2097 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002098 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002099 return RunGit(cmd) != ''
2100
2101
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002103 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002105 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 Updates changelog with metadata (e.g. pointer to review).
2107 Pushes/dcommits the code upstream.
2108 Updates review and closes.
2109 """
2110 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2111 help='bypass upload presubmit hook')
2112 parser.add_option('-m', dest='message',
2113 help="override review description")
2114 parser.add_option('-f', action='store_true', dest='force',
2115 help="force yes to questions (don't prompt)")
2116 parser.add_option('-c', dest='contributor',
2117 help="external contributor for patch (appended to " +
2118 "description and used as author for git). Should be " +
2119 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002120 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002121 (options, args) = parser.parse_args(args)
2122 cl = Changelist()
2123
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002124 current = cl.GetBranch()
2125 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2126 if not settings.GetIsGitSvn() and remote == '.':
2127 print
2128 print 'Attempting to push branch %r into another local branch!' % current
2129 print
2130 print 'Either reparent this branch on top of origin/master:'
2131 print ' git reparent-branch --root'
2132 print
2133 print 'OR run `git rebase-update` if you think the parent branch is already'
2134 print 'committed.'
2135 print
2136 print ' Current parent: %r' % upstream_branch
2137 return 1
2138
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002139 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002140 # Default to merging against our best guess of the upstream branch.
2141 args = [cl.GetUpstreamBranch()]
2142
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002143 if options.contributor:
2144 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2145 print "Please provide contibutor as 'First Last <email@example.com>'"
2146 return 1
2147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002149 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002150
sbc@chromium.org71437c02015-04-09 19:29:40 +00002151 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 return 1
2153
2154 # This rev-list syntax means "show all commits not in my branch that
2155 # are in base_branch".
2156 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2157 base_branch]).splitlines()
2158 if upstream_commits:
2159 print ('Base branch "%s" has %d commits '
2160 'not in this branch.' % (base_branch, len(upstream_commits)))
2161 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2162 return 1
2163
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002164 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002165 svn_head = None
2166 if cmd == 'dcommit' or base_has_submodules:
2167 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2168 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002169
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002170 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002171 # If the base_head is a submodule merge commit, the first parent of the
2172 # base_head should be a git-svn commit, which is what we're interested in.
2173 base_svn_head = base_branch
2174 if base_has_submodules:
2175 base_svn_head += '^1'
2176
2177 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002178 if extra_commits:
2179 print ('This branch has %d additional commits not upstreamed yet.'
2180 % len(extra_commits.splitlines()))
2181 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2182 'before attempting to %s.' % (base_branch, cmd))
2183 return 1
2184
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002185 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002186 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002187 author = None
2188 if options.contributor:
2189 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002190 hook_results = cl.RunHook(
2191 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002192 may_prompt=not options.force,
2193 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002194 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002195 if not hook_results.should_continue():
2196 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002198 # Check the tree status if the tree status URL is set.
2199 status = GetTreeStatus()
2200 if 'closed' == status:
2201 print('The tree is closed. Please wait for it to reopen. Use '
2202 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2203 return 1
2204 elif 'unknown' == status:
2205 print('Unable to determine tree status. Please verify manually and '
2206 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2207 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002208 else:
2209 breakpad.SendStack(
2210 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002211 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2212 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002213 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002214
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002215 change_desc = ChangeDescription(options.message)
2216 if not change_desc.description and cl.GetIssue():
2217 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002218
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002219 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002220 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002221 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002222 else:
2223 print 'No description set.'
2224 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2225 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002226
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002227 # Keep a separate copy for the commit message, because the commit message
2228 # contains the link to the Rietveld issue, while the Rietveld message contains
2229 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002230 # Keep a separate copy for the commit message.
2231 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002232 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002233
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002234 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002235 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002236 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002237 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002238 commit_desc.append_footer('Patch from %s.' % options.contributor)
2239
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002240 print('Description:')
2241 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002242
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002243 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002244 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002245 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002246
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002247 # We want to squash all this branch's commits into one commit with the proper
2248 # description. We do this by doing a "reset --soft" to the base branch (which
2249 # keeps the working copy the same), then dcommitting that. If origin/master
2250 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2251 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002252 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002253 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2254 # Delete the branches if they exist.
2255 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2256 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2257 result = RunGitWithCode(showref_cmd)
2258 if result[0] == 0:
2259 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002260
2261 # We might be in a directory that's present in this branch but not in the
2262 # trunk. Move up to the top of the tree so that git commands that expect a
2263 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002264 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002265 if rel_base_path:
2266 os.chdir(rel_base_path)
2267
2268 # Stuff our change into the merge branch.
2269 # We wrap in a try...finally block so if anything goes wrong,
2270 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002271 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002272 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002273 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002274 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002275 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002276 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002277 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002278 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002279 RunGit(
2280 [
2281 'commit', '--author', options.contributor,
2282 '-m', commit_desc.description,
2283 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002284 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002285 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002286 if base_has_submodules:
2287 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2288 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2289 RunGit(['checkout', CHERRY_PICK_BRANCH])
2290 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002291 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002292 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002293 pending_prefix = settings.GetPendingRefPrefix()
2294 if not pending_prefix or branch.startswith(pending_prefix):
2295 # If not using refs/pending/heads/* at all, or target ref is already set
2296 # to pending, then push to the target ref directly.
2297 retcode, output = RunGitWithCode(
2298 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002299 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002300 else:
2301 # Cherry-pick the change on top of pending ref and then push it.
2302 assert branch.startswith('refs/'), branch
2303 assert pending_prefix[-1] == '/', pending_prefix
2304 pending_ref = pending_prefix + branch[len('refs/'):]
2305 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002306 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002307 if retcode == 0:
2308 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309 else:
2310 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002311 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002312 'svn', 'dcommit',
2313 '-C%s' % options.similarity,
2314 '--no-rebase', '--rmdir',
2315 ]
2316 if settings.GetForceHttpsCommitUrl():
2317 # Allow forcing https commit URLs for some projects that don't allow
2318 # committing to http URLs (like Google Code).
2319 remote_url = cl.GetGitSvnRemoteUrl()
2320 if urlparse.urlparse(remote_url).scheme == 'http':
2321 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002322 cmd_args.append('--commit-url=%s' % remote_url)
2323 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002324 if 'Committed r' in output:
2325 revision = re.match(
2326 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2327 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002328 finally:
2329 # And then swap back to the original branch and clean up.
2330 RunGit(['checkout', '-q', cl.GetBranch()])
2331 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002332 if base_has_submodules:
2333 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002335 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002336 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002337 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002338
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002339 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002340 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002341 try:
2342 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2343 # We set pushed_to_pending to False, since it made it all the way to the
2344 # real ref.
2345 pushed_to_pending = False
2346 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002347 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002348
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002349 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002350 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002351 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002352 if not to_pending:
2353 if viewvc_url and revision:
2354 change_desc.append_footer(
2355 'Committed: %s%s' % (viewvc_url, revision))
2356 elif revision:
2357 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358 print ('Closing issue '
2359 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002360 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002361 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002362 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002363 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002364 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002365 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002366 if options.bypass_hooks:
2367 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2368 else:
2369 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002370 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002371 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002372
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002373 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002374 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2375 print 'The commit is in the pending queue (%s).' % pending_ref
2376 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002377 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002378 'footer.' % branch)
2379
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002380 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2381 if os.path.isfile(hook):
2382 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002383
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002384 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002385
2386
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002387def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2388 print
2389 print 'Waiting for commit to be landed on %s...' % real_ref
2390 print '(If you are impatient, you may Ctrl-C once without harm)'
2391 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2392 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2393
2394 loop = 0
2395 while True:
2396 sys.stdout.write('fetching (%d)... \r' % loop)
2397 sys.stdout.flush()
2398 loop += 1
2399
2400 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2401 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2402 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2403 for commit in commits.splitlines():
2404 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2405 print 'Found commit on %s' % real_ref
2406 return commit
2407
2408 current_rev = to_rev
2409
2410
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002411def PushToGitPending(remote, pending_ref, upstream_ref):
2412 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2413
2414 Returns:
2415 (retcode of last operation, output log of last operation).
2416 """
2417 assert pending_ref.startswith('refs/'), pending_ref
2418 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2419 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2420 code = 0
2421 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002422 max_attempts = 3
2423 attempts_left = max_attempts
2424 while attempts_left:
2425 if attempts_left != max_attempts:
2426 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2427 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002428
2429 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002430 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002431 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002432 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002433 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002434 print 'Fetch failed with exit code %d.' % code
2435 if out.strip():
2436 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002437 continue
2438
2439 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002440 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002441 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002442 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002443 if code:
2444 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002445 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2446 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002447 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2448 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002449 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002450 return code, out
2451
2452 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002453 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002454 code, out = RunGitWithCode(
2455 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2456 if code == 0:
2457 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002458 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002459 return code, out
2460
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002461 print 'Push failed with exit code %d.' % code
2462 if out.strip():
2463 print out.strip()
2464 if IsFatalPushFailure(out):
2465 print (
2466 'Fatal push error. Make sure your .netrc credentials and git '
2467 'user.email are correct and you have push access to the repo.')
2468 return code, out
2469
2470 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002471 return code, out
2472
2473
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002474def IsFatalPushFailure(push_stdout):
2475 """True if retrying push won't help."""
2476 return '(prohibited by Gerrit)' in push_stdout
2477
2478
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002479@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002480def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002481 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002482 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002483 message = """This doesn't appear to be an SVN repository.
2484If your project has a git mirror with an upstream SVN master, you probably need
2485to run 'git svn init', see your project's git mirror documentation.
2486If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002487to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002488Choose wisely, if you get this wrong, your commit might appear to succeed but
2489will instead be silently ignored."""
2490 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002491 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002492 return SendUpstream(parser, args, 'dcommit')
2493
2494
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002495@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002496def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002497 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002498 if settings.GetIsGitSvn():
2499 print('This appears to be an SVN repository.')
2500 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002501 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002502 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002503
2504
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002505@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002506def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002507 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002508 parser.add_option('-b', dest='newbranch',
2509 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002510 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002511 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002512 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2513 help='Change to the directory DIR immediately, '
2514 'before doing anything else.')
2515 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002516 help='failed patches spew .rej files rather than '
2517 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002518 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2519 help="don't commit after patch applies")
2520 (options, args) = parser.parse_args(args)
2521 if len(args) != 1:
2522 parser.print_help()
2523 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002524 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002525
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002526 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002527 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002528 return 1
2529
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002530 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002531 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002532
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002533 if options.newbranch:
2534 if options.force:
2535 RunGit(['branch', '-D', options.newbranch],
2536 stderr=subprocess2.PIPE, error_ok=True)
2537 RunGit(['checkout', '-b', options.newbranch,
2538 Changelist().GetUpstreamBranch()])
2539
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002540 return PatchIssue(issue_arg, options.reject, options.nocommit,
2541 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002542
2543
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002544def PatchIssue(issue_arg, reject, nocommit, directory):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002545 # There's a "reset --hard" when failing to apply the patch. In order
2546 # not to destroy users' data, make sure the tree is not dirty here.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002547 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002548
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002549 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002550 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002551 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002552 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002553 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002554 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002555 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002556 # Assume it's a URL to the patch. Default to https.
2557 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002558 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002559 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002560 DieWithError('Must pass an issue ID or full URL for '
2561 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002562 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002563 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002564 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002565
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002566 # Switch up to the top-level directory, if necessary, in preparation for
2567 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002568 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002569 if top:
2570 os.chdir(top)
2571
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002572 # Git patches have a/ at the beginning of source paths. We strip that out
2573 # with a sed script rather than the -p flag to patch so we can feed either
2574 # Git or svn-style patches into the same apply command.
2575 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002576 try:
2577 patch_data = subprocess2.check_output(
2578 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2579 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002580 DieWithError('Git patch mungling failed.')
2581 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002583 # We use "git apply" to apply the patch instead of "patch" so that we can
2584 # pick up file adds.
2585 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002586 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002587 if directory:
2588 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002589 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002590 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002591 elif IsGitVersionAtLeast('1.7.12'):
2592 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002593 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002594 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002595 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002596 except subprocess2.CalledProcessError:
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002597 RunGit(['reset', '--hard'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002598 DieWithError('Failed to apply the patch')
2599
2600 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002601 if not nocommit:
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002602 RunGit(['commit', '-m', ('patch from issue %(i)s at patchset '
2603 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2604 % {'i': issue, 'p': patchset})])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002605 cl = Changelist()
2606 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002607 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002608 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002609 else:
2610 print "Patch applied to index."
2611 return 0
2612
2613
2614def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002615 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002616 # Provide a wrapper for git svn rebase to help avoid accidental
2617 # git svn dcommit.
2618 # It's the only command that doesn't use parser at all since we just defer
2619 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002620
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002621 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622
2623
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002624def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002625 """Fetches the tree status and returns either 'open', 'closed',
2626 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002627 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002628 if url:
2629 status = urllib2.urlopen(url).read().lower()
2630 if status.find('closed') != -1 or status == '0':
2631 return 'closed'
2632 elif status.find('open') != -1 or status == '1':
2633 return 'open'
2634 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002635 return 'unset'
2636
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002638def GetTreeStatusReason():
2639 """Fetches the tree status from a json url and returns the message
2640 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002641 url = settings.GetTreeStatusUrl()
2642 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002643 connection = urllib2.urlopen(json_url)
2644 status = json.loads(connection.read())
2645 connection.close()
2646 return status['message']
2647
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002648
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002649def GetBuilderMaster(bot_list):
2650 """For a given builder, fetch the master from AE if available."""
2651 map_url = 'https://builders-map.appspot.com/'
2652 try:
2653 master_map = json.load(urllib2.urlopen(map_url))
2654 except urllib2.URLError as e:
2655 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2656 (map_url, e))
2657 except ValueError as e:
2658 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2659 if not master_map:
2660 return None, 'Failed to build master map.'
2661
2662 result_master = ''
2663 for bot in bot_list:
2664 builder = bot.split(':', 1)[0]
2665 master_list = master_map.get(builder, [])
2666 if not master_list:
2667 return None, ('No matching master for builder %s.' % builder)
2668 elif len(master_list) > 1:
2669 return None, ('The builder name %s exists in multiple masters %s.' %
2670 (builder, master_list))
2671 else:
2672 cur_master = master_list[0]
2673 if not result_master:
2674 result_master = cur_master
2675 elif result_master != cur_master:
2676 return None, 'The builders do not belong to the same master.'
2677 return result_master, None
2678
2679
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002680def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002681 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002682 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002683 status = GetTreeStatus()
2684 if 'unset' == status:
2685 print 'You must configure your tree status URL by running "git cl config".'
2686 return 2
2687
2688 print "The tree is %s" % status
2689 print
2690 print GetTreeStatusReason()
2691 if status != 'open':
2692 return 1
2693 return 0
2694
2695
maruel@chromium.org15192402012-09-06 12:38:29 +00002696def CMDtry(parser, args):
2697 """Triggers a try job through Rietveld."""
2698 group = optparse.OptionGroup(parser, "Try job options")
2699 group.add_option(
2700 "-b", "--bot", action="append",
2701 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2702 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002703 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002704 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002705 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002706 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002707 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002708 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002709 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002710 "-r", "--revision",
2711 help="Revision to use for the try job; default: the "
2712 "revision will be determined by the try server; see "
2713 "its waterfall for more info")
2714 group.add_option(
2715 "-c", "--clobber", action="store_true", default=False,
2716 help="Force a clobber before building; e.g. don't do an "
2717 "incremental build")
2718 group.add_option(
2719 "--project",
2720 help="Override which project to use. Projects are defined "
2721 "server-side to define what default bot set to use")
2722 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002723 "-n", "--name", help="Try job name; default to current branch name")
2724 parser.add_option_group(group)
2725 options, args = parser.parse_args(args)
2726
2727 if args:
2728 parser.error('Unknown arguments: %s' % args)
2729
2730 cl = Changelist()
2731 if not cl.GetIssue():
2732 parser.error('Need to upload first')
2733
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002734 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002735 if props.get('closed'):
2736 parser.error('Cannot send tryjobs for a closed CL')
2737
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002738 if props.get('private'):
2739 parser.error('Cannot use trybots with private issue')
2740
maruel@chromium.org15192402012-09-06 12:38:29 +00002741 if not options.name:
2742 options.name = cl.GetBranch()
2743
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002744 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002745 options.master, err_msg = GetBuilderMaster(options.bot)
2746 if err_msg:
2747 parser.error('Tryserver master cannot be found because: %s\n'
2748 'Please manually specify the tryserver master'
2749 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002750
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002751 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002752 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002753 if not options.bot:
2754 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002755
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002756 # Get try masters from PRESUBMIT.py files.
2757 masters = presubmit_support.DoGetTryMasters(
2758 change,
2759 change.LocalPaths(),
2760 settings.GetRoot(),
2761 None,
2762 None,
2763 options.verbose,
2764 sys.stdout)
2765 if masters:
2766 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002767
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002768 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2769 options.bot = presubmit_support.DoGetTrySlaves(
2770 change,
2771 change.LocalPaths(),
2772 settings.GetRoot(),
2773 None,
2774 None,
2775 options.verbose,
2776 sys.stdout)
2777 if not options.bot:
2778 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002779
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002780 builders_and_tests = {}
2781 # TODO(machenbach): The old style command-line options don't support
2782 # multiple try masters yet.
2783 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2784 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2785
2786 for bot in old_style:
2787 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002788 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002789 elif ',' in bot:
2790 parser.error('Specify one bot per --bot flag')
2791 else:
2792 builders_and_tests.setdefault(bot, []).append('defaulttests')
2793
2794 for bot, tests in new_style:
2795 builders_and_tests.setdefault(bot, []).extend(tests)
2796
2797 # Return a master map with one master to be backwards compatible. The
2798 # master name defaults to an empty string, which will cause the master
2799 # not to be set on rietveld (deprecated).
2800 return {options.master: builders_and_tests}
2801
2802 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002803
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002804 for builders in masters.itervalues():
2805 if any('triggered' in b for b in builders):
2806 print >> sys.stderr, (
2807 'ERROR You are trying to send a job to a triggered bot. This type of'
2808 ' bot requires an\ninitial job from a parent (usually a builder). '
2809 'Instead send your job to the parent.\n'
2810 'Bot list: %s' % builders)
2811 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002812
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002813 patchset = cl.GetMostRecentPatchset()
2814 if patchset and patchset != cl.GetPatchset():
2815 print(
2816 '\nWARNING Mismatch between local config and server. Did a previous '
2817 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2818 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002819 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002820 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002821 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002822 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002823 except urllib2.HTTPError, e:
2824 if e.code == 404:
2825 print('404 from rietveld; '
2826 'did you mean to use "git try" instead of "git cl try"?')
2827 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002828 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002829
2830 for (master, builders) in masters.iteritems():
2831 if master:
2832 print 'Master: %s' % master
2833 length = max(len(builder) for builder in builders)
2834 for builder in sorted(builders):
2835 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002836 return 0
2837
2838
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002839@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002841 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002842 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002843 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002844 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002846 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002847 if args:
2848 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002849 branch = cl.GetBranch()
2850 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002851 cl = Changelist()
2852 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002853
2854 # Clear configured merge-base, if there is one.
2855 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002856 else:
2857 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002858 return 0
2859
2860
thestig@chromium.org00858c82013-12-02 23:08:03 +00002861def CMDweb(parser, args):
2862 """Opens the current CL in the web browser."""
2863 _, args = parser.parse_args(args)
2864 if args:
2865 parser.error('Unrecognized args: %s' % ' '.join(args))
2866
2867 issue_url = Changelist().GetIssueURL()
2868 if not issue_url:
2869 print >> sys.stderr, 'ERROR No issue to open'
2870 return 1
2871
2872 webbrowser.open(issue_url)
2873 return 0
2874
2875
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002876def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002877 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002878 _, args = parser.parse_args(args)
2879 if args:
2880 parser.error('Unrecognized args: %s' % ' '.join(args))
2881 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002882 props = cl.GetIssueProperties()
2883 if props.get('private'):
2884 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002885 cl.SetFlag('commit', '1')
2886 return 0
2887
2888
groby@chromium.org411034a2013-02-26 15:12:01 +00002889def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002890 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002891 _, args = parser.parse_args(args)
2892 if args:
2893 parser.error('Unrecognized args: %s' % ' '.join(args))
2894 cl = Changelist()
2895 # Ensure there actually is an issue to close.
2896 cl.GetDescription()
2897 cl.CloseIssue()
2898 return 0
2899
2900
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002901def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00002902 """Shows differences between local tree and last upload."""
wychen@chromium.org16ca29c2015-04-02 22:26:55 +00002903 parser.parse_args(args)
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002904
2905 # Uncommitted (staged and unstaged) changes will be destroyed by
2906 # "git reset --hard" if there are merging conflicts in PatchIssue().
2907 # Staged changes would be committed along with the patch from last
2908 # upload, hence counted toward the "last upload" side in the final
2909 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002910 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002911 return 1
2912
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002913 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002914 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002915 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002916 if not issue:
2917 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002918 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002919 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002920
2921 # Create a new branch based on the merge-base
2922 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2923 try:
2924 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002925 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002926 if rtn != 0:
2927 return rtn
2928
wychen@chromium.org06928532015-02-03 02:11:29 +00002929 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002930 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00002931 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002932 finally:
2933 RunGit(['checkout', '-q', branch])
2934 RunGit(['branch', '-D', TMP_BRANCH])
2935
2936 return 0
2937
2938
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002939def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00002940 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002941 parser.add_option(
2942 '--no-color',
2943 action='store_true',
2944 help='Use this option to disable color output')
2945 options, args = parser.parse_args(args)
2946
2947 author = RunGit(['config', 'user.email']).strip() or None
2948
2949 cl = Changelist()
2950
2951 if args:
2952 if len(args) > 1:
2953 parser.error('Unknown args')
2954 base_branch = args[0]
2955 else:
2956 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002957 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002958
2959 change = cl.GetChange(base_branch, None)
2960 return owners_finder.OwnersFinder(
2961 [f.LocalPath() for f in
2962 cl.GetChange(base_branch, None).AffectedFiles()],
2963 change.RepositoryRoot(), author,
2964 fopen=file, os_path=os.path, glob=glob.glob,
2965 disable_color=options.no_color).run()
2966
2967
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002968def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
2969 """Generates a diff command."""
2970 # Generate diff for the current branch's changes.
2971 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
2972 upstream_commit, '--' ]
2973
2974 if args:
2975 for arg in args:
2976 if os.path.isdir(arg):
2977 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
2978 elif os.path.isfile(arg):
2979 diff_cmd.append(arg)
2980 else:
2981 DieWithError('Argument "%s" is not a file or a directory' % arg)
2982 else:
2983 diff_cmd.extend('*' + ext for ext in extensions)
2984
2985 return diff_cmd
2986
2987
enne@chromium.org555cfe42014-01-29 18:21:39 +00002988@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002989def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002990 """Runs clang-format on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00002991 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002992 parser.add_option('--full', action='store_true',
2993 help='Reformat the full content of all touched files')
2994 parser.add_option('--dry-run', action='store_true',
2995 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002996 parser.add_option('--diff', action='store_true',
2997 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002998 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002999
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003000 # git diff generates paths against the root of the repository. Change
3001 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003002 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003003 if rel_base_path:
3004 os.chdir(rel_base_path)
3005
digit@chromium.org29e47272013-05-17 17:01:46 +00003006 # Grab the merge-base commit, i.e. the upstream commit of the current
3007 # branch when it was created or the last time it was rebased. This is
3008 # to cover the case where the user may have called "git fetch origin",
3009 # moving the origin branch to a newer commit, but hasn't rebased yet.
3010 upstream_commit = None
3011 cl = Changelist()
3012 upstream_branch = cl.GetUpstreamBranch()
3013 if upstream_branch:
3014 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3015 upstream_commit = upstream_commit.strip()
3016
3017 if not upstream_commit:
3018 DieWithError('Could not find base commit for this branch. '
3019 'Are you in detached state?')
3020
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003021 if opts.full:
3022 # Only list the names of modified files.
3023 clang_diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003024 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003025 # Only generate context-less patches.
3026 clang_diff_type = '-U0'
3027
3028 diff_cmd = BuildGitDiffCmd(clang_diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003029 diff_output = RunGit(diff_cmd)
3030
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003031 top_dir = os.path.normpath(
3032 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3033
3034 # Locate the clang-format binary in the checkout
3035 try:
3036 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3037 except clang_format.NotFoundError, e:
3038 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003039
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003040 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3041 # formatted. This is used to block during the presubmit.
3042 return_value = 0
3043
digit@chromium.org29e47272013-05-17 17:01:46 +00003044 if opts.full:
3045 # diff_output is a list of files to send to clang-format.
3046 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003047 if files:
3048 cmd = [clang_format_tool]
3049 if not opts.dry_run and not opts.diff:
3050 cmd.append('-i')
3051 stdout = RunCommand(cmd + files, cwd=top_dir)
3052 if opts.diff:
3053 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003054 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003055 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003056 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003057 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003058 try:
3059 script = clang_format.FindClangFormatScriptInChromiumTree(
3060 'clang-format-diff.py')
3061 except clang_format.NotFoundError, e:
3062 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003063
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003064 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003065 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003066 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003067
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003068 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003069 if opts.diff:
3070 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003071 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003072 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003073
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003074 # Build a diff command that only operates on dart files. dart's formatter
3075 # does not have the nice property of only operating on modified chunks, so
3076 # hard code full.
3077 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3078 args, ['.dart'])
3079 dart_diff_output = RunGit(dart_diff_cmd)
3080 if dart_diff_output:
3081 try:
3082 command = [dart_format.FindDartFmtToolInChromiumTree()]
3083 if not opts.dry_run and not opts.diff:
3084 command.append('-w')
3085 command.extend(dart_diff_output.splitlines())
3086
3087 stdout = RunCommand(command, cwd=top_dir, env=env)
3088 if opts.dry_run and stdout:
3089 return_value = 2
3090 except dart_format.NotFoundError as e:
3091 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3092 'this checkout.')
3093
3094 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003095
3096
maruel@chromium.org29404b52014-09-08 22:58:00 +00003097def CMDlol(parser, args):
3098 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003099 print zlib.decompress(base64.b64decode(
3100 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3101 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3102 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3103 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003104 return 0
3105
3106
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003107class OptionParser(optparse.OptionParser):
3108 """Creates the option parse and add --verbose support."""
3109 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003110 optparse.OptionParser.__init__(
3111 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003112 self.add_option(
3113 '-v', '--verbose', action='count', default=0,
3114 help='Use 2 times for more debugging info')
3115
3116 def parse_args(self, args=None, values=None):
3117 options, args = optparse.OptionParser.parse_args(self, args, values)
3118 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3119 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3120 return options, args
3121
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003122
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003123def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003124 if sys.hexversion < 0x02060000:
3125 print >> sys.stderr, (
3126 '\nYour python version %s is unsupported, please upgrade.\n' %
3127 sys.version.split(' ', 1)[0])
3128 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003129
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003130 # Reload settings.
3131 global settings
3132 settings = Settings()
3133
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003134 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003135 dispatcher = subcommand.CommandDispatcher(__name__)
3136 try:
3137 return dispatcher.execute(OptionParser(), argv)
3138 except urllib2.HTTPError, e:
3139 if e.code != 500:
3140 raise
3141 DieWithError(
3142 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3143 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003144 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003145
3146
3147if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003148 # These affect sys.stdout so do it outside of main() to simplify mocks in
3149 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003150 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003151 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003152 try:
3153 sys.exit(main(sys.argv[1:]))
3154 except KeyboardInterrupt:
3155 sys.stderr.write('interrupted\n')
3156 sys.exit(1)