blob: 1f4b01b039abdcf58f04786ccd8c8cd4b1e92e1a [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
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000038import git_common
39import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000041import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000043import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000044import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import watchlists
46
maruel@chromium.org0633fb42013-08-16 20:06:14 +000047__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000049DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000050POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000052GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000053CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054
thestig@chromium.org44202a22014-03-11 19:22:18 +000055# Valid extensions for files we want to lint.
56DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
57DEFAULT_LINT_IGNORE_REGEX = r"$^"
58
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000059# Shortcut since it quickly becomes redundant.
60Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000061
maruel@chromium.orgddd59412011-11-30 14:20:38 +000062# Initialized in main()
63settings = None
64
65
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000067 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 sys.exit(1)
69
70
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000071def GetNoGitPagerEnv():
72 env = os.environ.copy()
73 # 'cat' is a magical git string that disables pagers on all platforms.
74 env['GIT_PAGER'] = 'cat'
75 return env
76
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000077
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000080 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000081 except subprocess2.CalledProcessError as e:
82 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000085 'Command "%s" failed.\n%s' % (
86 ' '.join(args), error_message or e.stdout or ''))
87 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088
89
90def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000091 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000092 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093
94
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000095def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000097 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000098 if suppress_stderr:
99 stderr = subprocess2.VOID
100 else:
101 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000102 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000103 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000104 stdout=subprocess2.PIPE,
105 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000106 return code, out[0]
107 except ValueError:
108 # When the subprocess fails, it returns None. That triggers a ValueError
109 # when trying to unpack the return value into (out, code).
110 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000113def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000114 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000115 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000116 return (version.startswith(prefix) and
117 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000118
119
maruel@chromium.org90541732011-04-01 17:54:18 +0000120def ask_for_data(prompt):
121 try:
122 return raw_input(prompt)
123 except KeyboardInterrupt:
124 # Hide the exception.
125 sys.exit(1)
126
127
iannucci@chromium.org79540052012-10-19 23:15:26 +0000128def git_set_branch_value(key, value):
129 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000130 if not branch:
131 return
132
133 cmd = ['config']
134 if isinstance(value, int):
135 cmd.append('--int')
136 git_key = 'branch.%s.%s' % (branch, key)
137 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138
139
140def git_get_branch_default(key, default):
141 branch = Changelist().GetBranch()
142 if branch:
143 git_key = 'branch.%s.%s' % (branch, key)
144 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
145 try:
146 return int(stdout.strip())
147 except ValueError:
148 pass
149 return default
150
151
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152def add_git_similarity(parser):
153 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 help='Sets the percentage that a pair of files need to match in order to'
156 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 parser.add_option(
158 '--find-copies', action='store_true',
159 help='Allows git to look for copies.')
160 parser.add_option(
161 '--no-find-copies', action='store_false', dest='find_copies',
162 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000163
164 old_parser_args = parser.parse_args
165 def Parse(args):
166 options, args = old_parser_args(args)
167
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000168 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000170 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171 print('Note: Saving similarity of %d%% in git config.'
172 % options.similarity)
173 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000174
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 options.similarity = max(0, min(options.similarity, 100))
176
177 if options.find_copies is None:
178 options.find_copies = bool(
179 git_get_branch_default('git-find-copies', True))
180 else:
181 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182
183 print('Using %d%% similarity for rename/copy detection. '
184 'Override with --similarity.' % options.similarity)
185
186 return options, args
187 parser.parse_args = Parse
188
189
ukai@chromium.org259e4682012-10-25 07:36:33 +0000190def is_dirty_git_tree(cmd):
191 # Make sure index is up-to-date before running diff-index.
192 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
193 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
194 if dirty:
195 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
196 print 'Uncommitted files: (git diff-index --name-status HEAD)'
197 print dirty[:4096]
198 if len(dirty) > 4096:
199 print '... (run "git diff-index --name-status HEAD" to see full output).'
200 return True
201 return False
202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000203
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
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000284 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285
286 def LazyUpdateIfNeeded(self):
287 """Updates the settings from a codereview.settings file, if available."""
288 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000289 # The only value that actually changes the behavior is
290 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000291 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000292 error_ok=True
293 ).strip().lower()
294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000296 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000298 # set updated to True to avoid infinite calling loop
299 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000300 self.updated = True
301 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 self.updated = True
303
304 def GetDefaultServerUrl(self, error_ok=False):
305 if not self.default_server:
306 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000307 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000308 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309 if error_ok:
310 return self.default_server
311 if not self.default_server:
312 error_message = ('Could not find settings file. You must configure '
313 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000314 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000315 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316 return self.default_server
317
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000318 @staticmethod
319 def GetRelativeRoot():
320 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000323 if self.root is None:
324 self.root = os.path.abspath(self.GetRelativeRoot())
325 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326
327 def GetIsGitSvn(self):
328 """Return true if this repo looks like it's using git-svn."""
329 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000330 if self.GetPendingRefPrefix():
331 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
332 self.is_git_svn = False
333 else:
334 # If you have any "svn-remote.*" config keys, we think you're using svn.
335 self.is_git_svn = RunGitWithCode(
336 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337 return self.is_git_svn
338
339 def GetSVNBranch(self):
340 if self.svn_branch is None:
341 if not self.GetIsGitSvn():
342 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
343
344 # Try to figure out which remote branch we're based on.
345 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000346 # 1) iterate through our branch history and find the svn URL.
347 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348
349 # regexp matching the git-svn line that contains the URL.
350 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
351
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000352 # We don't want to go through all of history, so read a line from the
353 # pipe at a time.
354 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000355 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000356 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
357 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000358 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000359 for line in proc.stdout:
360 match = git_svn_re.match(line)
361 if match:
362 url = match.group(1)
363 proc.stdout.close() # Cut pipe.
364 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000366 if url:
367 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
368 remotes = RunGit(['config', '--get-regexp',
369 r'^svn-remote\..*\.url']).splitlines()
370 for remote in remotes:
371 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000373 remote = match.group(1)
374 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000375 rewrite_root = RunGit(
376 ['config', 'svn-remote.%s.rewriteRoot' % remote],
377 error_ok=True).strip()
378 if rewrite_root:
379 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000380 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000381 ['config', 'svn-remote.%s.fetch' % remote],
382 error_ok=True).strip()
383 if fetch_spec:
384 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
385 if self.svn_branch:
386 break
387 branch_spec = RunGit(
388 ['config', 'svn-remote.%s.branches' % remote],
389 error_ok=True).strip()
390 if branch_spec:
391 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
392 if self.svn_branch:
393 break
394 tag_spec = RunGit(
395 ['config', 'svn-remote.%s.tags' % remote],
396 error_ok=True).strip()
397 if tag_spec:
398 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
399 if self.svn_branch:
400 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401
402 if not self.svn_branch:
403 DieWithError('Can\'t guess svn branch -- try specifying it on the '
404 'command line')
405
406 return self.svn_branch
407
408 def GetTreeStatusUrl(self, error_ok=False):
409 if not self.tree_status_url:
410 error_message = ('You must configure your tree status URL by running '
411 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000412 self.tree_status_url = self._GetRietveldConfig(
413 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 return self.tree_status_url
415
416 def GetViewVCUrl(self):
417 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000418 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 return self.viewvc_url
420
rmistry@google.com90752582014-01-14 21:04:50 +0000421 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000422 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000423
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000424 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000425 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000426
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000427 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000428 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000429
ukai@chromium.orge8077812012-02-03 03:41:46 +0000430 def GetIsGerrit(self):
431 """Return true if this repo is assosiated with gerrit code review system."""
432 if self.is_gerrit is None:
433 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
434 return self.is_gerrit
435
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000436 def GetGitEditor(self):
437 """Return the editor specified in the git config, or None if none is."""
438 if self.git_editor is None:
439 self.git_editor = self._GetConfig('core.editor', error_ok=True)
440 return self.git_editor or None
441
thestig@chromium.org44202a22014-03-11 19:22:18 +0000442 def GetLintRegex(self):
443 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
444 DEFAULT_LINT_REGEX)
445
446 def GetLintIgnoreRegex(self):
447 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
448 DEFAULT_LINT_IGNORE_REGEX)
449
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000450 def GetProject(self):
451 if not self.project:
452 self.project = self._GetRietveldConfig('project', error_ok=True)
453 return self.project
454
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000455 def GetPendingRefPrefix(self):
456 if not self.pending_ref_prefix:
457 self.pending_ref_prefix = self._GetRietveldConfig(
458 'pending-ref-prefix', error_ok=True)
459 return self.pending_ref_prefix
460
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000461 def _GetRietveldConfig(self, param, **kwargs):
462 return self._GetConfig('rietveld.' + param, **kwargs)
463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000464 def _GetConfig(self, param, **kwargs):
465 self.LazyUpdateIfNeeded()
466 return RunGit(['config', param], **kwargs).strip()
467
468
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469def ShortBranchName(branch):
470 """Convert a name like 'refs/heads/foo' to just 'foo'."""
471 return branch.replace('refs/heads/', '')
472
473
474class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000475 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000477 global settings
478 if not settings:
479 # Happens when git_cl.py is used as a utility library.
480 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 settings.GetDefaultServerUrl()
482 self.branchref = branchref
483 if self.branchref:
484 self.branch = ShortBranchName(self.branchref)
485 else:
486 self.branch = None
487 self.rietveld_server = None
488 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000489 self.lookedup_issue = False
490 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 self.has_description = False
492 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000493 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000495 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000496 self.cc = None
497 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000498 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000499 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000500
501 def GetCCList(self):
502 """Return the users cc'd on this CL.
503
504 Return is a string suitable for passing to gcl with the --cc flag.
505 """
506 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000507 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000508 more_cc = ','.join(self.watchers)
509 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
510 return self.cc
511
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000512 def GetCCListWithoutDefault(self):
513 """Return the users cc'd on this CL excluding default ones."""
514 if self.cc is None:
515 self.cc = ','.join(self.watchers)
516 return self.cc
517
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000518 def SetWatchers(self, watchers):
519 """Set the list of email addresses that should be cc'd based on the changed
520 files in this CL.
521 """
522 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000523
524 def GetBranch(self):
525 """Returns the short branch name, e.g. 'master'."""
526 if not self.branch:
527 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
528 self.branch = ShortBranchName(self.branchref)
529 return self.branch
530
531 def GetBranchRef(self):
532 """Returns the full branch name, e.g. 'refs/heads/master'."""
533 self.GetBranch() # Poke the lazy loader.
534 return self.branchref
535
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 @staticmethod
537 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000538 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539 e.g. 'origin', 'refs/heads/master'
540 """
541 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000542 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
543 error_ok=True).strip()
544 if upstream_branch:
545 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
546 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000547 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
548 error_ok=True).strip()
549 if upstream_branch:
550 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000551 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000552 # Fall back on trying a git-svn upstream branch.
553 if settings.GetIsGitSvn():
554 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000555 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000556 # Else, try to guess the origin remote.
557 remote_branches = RunGit(['branch', '-r']).split()
558 if 'origin/master' in remote_branches:
559 # Fall back on origin/master if it exits.
560 remote = 'origin'
561 upstream_branch = 'refs/heads/master'
562 elif 'origin/trunk' in remote_branches:
563 # Fall back on origin/trunk if it exists. Generally a shared
564 # git-svn clone
565 remote = 'origin'
566 upstream_branch = 'refs/heads/trunk'
567 else:
568 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569Either pass complete "git diff"-style arguments, like
570 git cl upload origin/master
571or verify this branch is set up to track another (via the --track argument to
572"git checkout -b ...").""")
573
574 return remote, upstream_branch
575
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000576 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000577 return git_common.get_or_create_merge_base(self.GetBranch(),
578 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580 def GetUpstreamBranch(self):
581 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000582 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 if remote is not '.':
584 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
585 self.upstream_branch = upstream_branch
586 return self.upstream_branch
587
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000588 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000589 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000590 remote, branch = None, self.GetBranch()
591 seen_branches = set()
592 while branch not in seen_branches:
593 seen_branches.add(branch)
594 remote, branch = self.FetchUpstreamTuple(branch)
595 branch = ShortBranchName(branch)
596 if remote != '.' or branch.startswith('refs/remotes'):
597 break
598 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000599 remotes = RunGit(['remote'], error_ok=True).split()
600 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000601 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000602 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000603 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000604 logging.warning('Could not determine which remote this change is '
605 'associated with, so defaulting to "%s". This may '
606 'not be what you want. You may prevent this message '
607 'by running "git svn info" as documented here: %s',
608 self._remote,
609 GIT_INSTRUCTIONS_URL)
610 else:
611 logging.warn('Could not determine which remote this change is '
612 'associated with. You may prevent this message by '
613 'running "git svn info" as documented here: %s',
614 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000615 branch = 'HEAD'
616 if branch.startswith('refs/remotes'):
617 self._remote = (remote, branch)
618 else:
619 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000620 return self._remote
621
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000622 def GitSanityChecks(self, upstream_git_obj):
623 """Checks git repo status and ensures diff is from local commits."""
624
625 # Verify the commit we're diffing against is in our current branch.
626 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
627 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
628 if upstream_sha != common_ancestor:
629 print >> sys.stderr, (
630 'ERROR: %s is not in the current branch. You may need to rebase '
631 'your tracking branch' % upstream_sha)
632 return False
633
634 # List the commits inside the diff, and verify they are all local.
635 commits_in_diff = RunGit(
636 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
637 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
638 remote_branch = remote_branch.strip()
639 if code != 0:
640 _, remote_branch = self.GetRemoteBranch()
641
642 commits_in_remote = RunGit(
643 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
644
645 common_commits = set(commits_in_diff) & set(commits_in_remote)
646 if common_commits:
647 print >> sys.stderr, (
648 'ERROR: Your diff contains %d commits already in %s.\n'
649 'Run "git log --oneline %s..HEAD" to get a list of commits in '
650 'the diff. If you are using a custom git flow, you can override'
651 ' the reference used for this check with "git config '
652 'gitcl.remotebranch <git-ref>".' % (
653 len(common_commits), remote_branch, upstream_git_obj))
654 return False
655 return True
656
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000657 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000658 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000659
660 Returns None if it is not set.
661 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000662 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
663 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665 def GetRemoteUrl(self):
666 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
667
668 Returns None if there is no remote.
669 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000670 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000671 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
672
673 # If URL is pointing to a local directory, it is probably a git cache.
674 if os.path.isdir(url):
675 url = RunGit(['config', 'remote.%s.url' % remote],
676 error_ok=True,
677 cwd=url).strip()
678 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000679
680 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000681 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000682 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000684 self.issue = int(issue) or None if issue else None
685 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686 return self.issue
687
688 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000689 if not self.rietveld_server:
690 # If we're on a branch then get the server potentially associated
691 # with that branch.
692 if self.GetIssue():
693 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
694 ['config', self._RietveldServer()], error_ok=True).strip())
695 if not self.rietveld_server:
696 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 return self.rietveld_server
698
699 def GetIssueURL(self):
700 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000701 if not self.GetIssue():
702 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
704
705 def GetDescription(self, pretty=False):
706 if not self.has_description:
707 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000708 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000709 try:
710 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000711 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000712 if e.code == 404:
713 DieWithError(
714 ('\nWhile fetching the description for issue %d, received a '
715 '404 (not found)\n'
716 'error. It is likely that you deleted this '
717 'issue on the server. If this is the\n'
718 'case, please run\n\n'
719 ' git cl issue 0\n\n'
720 'to clear the association with the deleted issue. Then run '
721 'this command again.') % issue)
722 else:
723 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000724 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000725 except urllib2.URLError as e:
726 print >> sys.stderr, (
727 'Warning: Failed to retrieve CL description due to network '
728 'failure.')
729 self.description = ''
730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 self.has_description = True
732 if pretty:
733 wrapper = textwrap.TextWrapper()
734 wrapper.initial_indent = wrapper.subsequent_indent = ' '
735 return wrapper.fill(self.description)
736 return self.description
737
738 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000739 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000740 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 patchset = RunGit(['config', self._PatchsetSetting()],
742 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000743 self.patchset = int(patchset) or None if patchset else None
744 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 return self.patchset
746
747 def SetPatchset(self, patchset):
748 """Set this branch's patchset. If patchset=0, clears the patchset."""
749 if patchset:
750 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000751 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 else:
753 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000754 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000755 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000756
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000757 def GetMostRecentPatchset(self):
758 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000759
760 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000761 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000762 '/download/issue%s_%s.diff' % (issue, patchset))
763
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000764 def GetIssueProperties(self):
765 if self._props is None:
766 issue = self.GetIssue()
767 if not issue:
768 self._props = {}
769 else:
770 self._props = self.RpcServer().get_issue_properties(issue, True)
771 return self._props
772
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000773 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000774 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 def SetIssue(self, issue):
777 """Set this branch's issue. If issue=0, clears the issue."""
778 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000779 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780 RunGit(['config', self._IssueSetting(), str(issue)])
781 if self.rietveld_server:
782 RunGit(['config', self._RietveldServer(), self.rietveld_server])
783 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000784 current_issue = self.GetIssue()
785 if current_issue:
786 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000787 self.issue = None
788 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000790 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000791 if not self.GitSanityChecks(upstream_branch):
792 DieWithError('\nGit sanity check failure')
793
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000795 if not root:
796 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000797 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000798
799 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000801 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000802 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000803 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000804 except subprocess2.CalledProcessError:
805 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000806 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000807 'This branch probably doesn\'t exist anymore. To reset the\n'
808 'tracking branch, please run\n'
809 ' git branch --set-upstream %s trunk\n'
810 'replacing trunk with origin/master or the relevant branch') %
811 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000812
maruel@chromium.org52424302012-08-29 15:14:30 +0000813 issue = self.GetIssue()
814 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000815 if issue:
816 description = self.GetDescription()
817 else:
818 # If the change was never uploaded, use the log messages of all commits
819 # up to the branch point, as git cl upload will prefill the description
820 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000821 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
822 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000823
824 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000825 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000826 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000827 name,
828 description,
829 absroot,
830 files,
831 issue,
832 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000833 author,
834 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000835
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000836 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000837 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000838
839 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000840 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000841 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000842 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000843 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000844 except presubmit_support.PresubmitFailure, e:
845 DieWithError(
846 ('%s\nMaybe your depot_tools is out of date?\n'
847 'If all fails, contact maruel@') % e)
848
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000849 def UpdateDescription(self, description):
850 self.description = description
851 return self.RpcServer().update_description(
852 self.GetIssue(), self.description)
853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000855 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000856 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000858 def SetFlag(self, flag, value):
859 """Patchset must match."""
860 if not self.GetPatchset():
861 DieWithError('The patchset needs to match. Send another patchset.')
862 try:
863 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000864 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000865 except urllib2.HTTPError, e:
866 if e.code == 404:
867 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
868 if e.code == 403:
869 DieWithError(
870 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
871 'match?') % (self.GetIssue(), self.GetPatchset()))
872 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000874 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 """Returns an upload.RpcServer() to access this review's rietveld instance.
876 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000877 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000878 self._rpc_server = rietveld.CachingRietveld(
879 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000880 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881
882 def _IssueSetting(self):
883 """Return the git setting that stores this change's issue."""
884 return 'branch.%s.rietveldissue' % self.GetBranch()
885
886 def _PatchsetSetting(self):
887 """Return the git setting that stores this change's most recent patchset."""
888 return 'branch.%s.rietveldpatchset' % self.GetBranch()
889
890 def _RietveldServer(self):
891 """Returns the git setting that stores this change's rietveld server."""
892 return 'branch.%s.rietveldserver' % self.GetBranch()
893
894
895def GetCodereviewSettingsInteractively():
896 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000897 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 server = settings.GetDefaultServerUrl(error_ok=True)
899 prompt = 'Rietveld server (host[:port])'
900 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000901 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902 if not server and not newserver:
903 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000904 if newserver:
905 newserver = gclient_utils.UpgradeToHttps(newserver)
906 if newserver != server:
907 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000909 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 prompt = caption
911 if initial:
912 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000913 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914 if new_val == 'x':
915 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000916 elif new_val:
917 if is_url:
918 new_val = gclient_utils.UpgradeToHttps(new_val)
919 if new_val != initial:
920 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000922 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000923 SetProperty(settings.GetDefaultPrivateFlag(),
924 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000926 'tree-status-url', False)
927 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000928 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929
930 # TODO: configure a default branch to diff against, rather than this
931 # svn-based hackery.
932
933
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000934class ChangeDescription(object):
935 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000936 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000937 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000938
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000939 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000940 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000941
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 @property # www.logilab.org/ticket/89786
943 def description(self): # pylint: disable=E0202
944 return '\n'.join(self._description_lines)
945
946 def set_description(self, desc):
947 if isinstance(desc, basestring):
948 lines = desc.splitlines()
949 else:
950 lines = [line.rstrip() for line in desc]
951 while lines and not lines[0]:
952 lines.pop(0)
953 while lines and not lines[-1]:
954 lines.pop(-1)
955 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000956
957 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000958 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000959 assert isinstance(reviewers, list), reviewers
960 if not reviewers:
961 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000962 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000963
agable@chromium.org42c20792013-09-12 17:34:49 +0000964 # Get the set of R= and TBR= lines and remove them from the desciption.
965 regexp = re.compile(self.R_LINE)
966 matches = [regexp.match(line) for line in self._description_lines]
967 new_desc = [l for i, l in enumerate(self._description_lines)
968 if not matches[i]]
969 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000970
agable@chromium.org42c20792013-09-12 17:34:49 +0000971 # Construct new unified R= and TBR= lines.
972 r_names = []
973 tbr_names = []
974 for match in matches:
975 if not match:
976 continue
977 people = cleanup_list([match.group(2).strip()])
978 if match.group(1) == 'TBR':
979 tbr_names.extend(people)
980 else:
981 r_names.extend(people)
982 for name in r_names:
983 if name not in reviewers:
984 reviewers.append(name)
985 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
986 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
987
988 # Put the new lines in the description where the old first R= line was.
989 line_loc = next((i for i, match in enumerate(matches) if match), -1)
990 if 0 <= line_loc < len(self._description_lines):
991 if new_tbr_line:
992 self._description_lines.insert(line_loc, new_tbr_line)
993 if new_r_line:
994 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000995 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000996 if new_r_line:
997 self.append_footer(new_r_line)
998 if new_tbr_line:
999 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001000
1001 def prompt(self):
1002 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001003 self.set_description([
1004 '# Enter a description of the change.',
1005 '# This will be displayed on the codereview site.',
1006 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001007 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001008 '--------------------',
1009 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001010
agable@chromium.org42c20792013-09-12 17:34:49 +00001011 regexp = re.compile(self.BUG_LINE)
1012 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001013 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001014 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001015 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001016 if not content:
1017 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001018 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001019
1020 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001021 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1022 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001023 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001024 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001025
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001026 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001027 if self._description_lines:
1028 # Add an empty line if either the last line or the new line isn't a tag.
1029 last_line = self._description_lines[-1]
1030 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1031 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1032 self._description_lines.append('')
1033 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001034
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001035 def get_reviewers(self):
1036 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001037 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1038 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001039 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001040
1041
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001042def get_approving_reviewers(props):
1043 """Retrieves the reviewers that approved a CL from the issue properties with
1044 messages.
1045
1046 Note that the list may contain reviewers that are not committer, thus are not
1047 considered by the CQ.
1048 """
1049 return sorted(
1050 set(
1051 message['sender']
1052 for message in props['messages']
1053 if message['approval'] and message['sender'] in props['reviewers']
1054 )
1055 )
1056
1057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058def FindCodereviewSettingsFile(filename='codereview.settings'):
1059 """Finds the given file starting in the cwd and going up.
1060
1061 Only looks up to the top of the repository unless an
1062 'inherit-review-settings-ok' file exists in the root of the repository.
1063 """
1064 inherit_ok_file = 'inherit-review-settings-ok'
1065 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001066 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1068 root = '/'
1069 while True:
1070 if filename in os.listdir(cwd):
1071 if os.path.isfile(os.path.join(cwd, filename)):
1072 return open(os.path.join(cwd, filename))
1073 if cwd == root:
1074 break
1075 cwd = os.path.dirname(cwd)
1076
1077
1078def LoadCodereviewSettingsFromFile(fileobj):
1079 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001080 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082 def SetProperty(name, setting, unset_error_ok=False):
1083 fullname = 'rietveld.' + name
1084 if setting in keyvals:
1085 RunGit(['config', fullname, keyvals[setting]])
1086 else:
1087 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1088
1089 SetProperty('server', 'CODE_REVIEW_SERVER')
1090 # Only server setting is required. Other settings can be absent.
1091 # In that case, we ignore errors raised during option deletion attempt.
1092 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001093 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1095 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001096 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001097 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1098 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001099 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001100 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001102 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001103 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1106 #should be of the form
1107 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1108 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1109 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1110 keyvals['ORIGIN_URL_CONFIG']])
1111
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001113def urlretrieve(source, destination):
1114 """urllib is broken for SSL connections via a proxy therefore we
1115 can't use urllib.urlretrieve()."""
1116 with open(destination, 'w') as f:
1117 f.write(urllib2.urlopen(source).read())
1118
1119
ukai@chromium.org712d6102013-11-27 00:52:58 +00001120def hasSheBang(fname):
1121 """Checks fname is a #! script."""
1122 with open(fname) as f:
1123 return f.read(2).startswith('#!')
1124
1125
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001126def DownloadHooks(force):
1127 """downloads hooks
1128
1129 Args:
1130 force: True to update hooks. False to install hooks if not present.
1131 """
1132 if not settings.GetIsGerrit():
1133 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001134 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001135 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1136 if not os.access(dst, os.X_OK):
1137 if os.path.exists(dst):
1138 if not force:
1139 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001140 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001141 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001142 if not hasSheBang(dst):
1143 DieWithError('Not a script: %s\n'
1144 'You need to download from\n%s\n'
1145 'into .git/hooks/commit-msg and '
1146 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001147 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1148 except Exception:
1149 if os.path.exists(dst):
1150 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001151 DieWithError('\nFailed to download hooks.\n'
1152 'You need to download from\n%s\n'
1153 'into .git/hooks/commit-msg and '
1154 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001155
1156
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001157@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001159 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001161 parser.add_option('--activate-update', action='store_true',
1162 help='activate auto-updating [rietveld] section in '
1163 '.git/config')
1164 parser.add_option('--deactivate-update', action='store_true',
1165 help='deactivate auto-updating [rietveld] section in '
1166 '.git/config')
1167 options, args = parser.parse_args(args)
1168
1169 if options.deactivate_update:
1170 RunGit(['config', 'rietveld.autoupdate', 'false'])
1171 return
1172
1173 if options.activate_update:
1174 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1175 return
1176
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 if len(args) == 0:
1178 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001179 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 return 0
1181
1182 url = args[0]
1183 if not url.endswith('codereview.settings'):
1184 url = os.path.join(url, 'codereview.settings')
1185
1186 # Load code review settings and download hooks (if available).
1187 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001188 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 return 0
1190
1191
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001192def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001193 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001194 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1195 branch = ShortBranchName(branchref)
1196 _, args = parser.parse_args(args)
1197 if not args:
1198 print("Current base-url:")
1199 return RunGit(['config', 'branch.%s.base-url' % branch],
1200 error_ok=False).strip()
1201 else:
1202 print("Setting base-url to %s" % args[0])
1203 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1204 error_ok=False).strip()
1205
1206
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001208 """Show status of changelists.
1209
1210 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001211 - Red not sent for review or broken
1212 - Blue waiting for review
1213 - Yellow waiting for you to reply to review
1214 - Green LGTM'ed
1215 - Magenta in the commit queue
1216 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001217
1218 Also see 'git cl comments'.
1219 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 parser.add_option('--field',
1221 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001222 parser.add_option('-f', '--fast', action='store_true',
1223 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001225 if args:
1226 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001229 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 if options.field.startswith('desc'):
1231 print cl.GetDescription()
1232 elif options.field == 'id':
1233 issueid = cl.GetIssue()
1234 if issueid:
1235 print issueid
1236 elif options.field == 'patch':
1237 patchset = cl.GetPatchset()
1238 if patchset:
1239 print patchset
1240 elif options.field == 'url':
1241 url = cl.GetIssueURL()
1242 if url:
1243 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001244 return 0
1245
1246 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1247 if not branches:
1248 print('No local branch found.')
1249 return 0
1250
1251 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001252 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001253 alignment = max(5, max(len(b) for b in branches))
1254 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001255 # Adhoc thread pool to request data concurrently.
1256 output = Queue.Queue()
1257
1258 # Silence upload.py otherwise it becomes unweldly.
1259 upload.verbosity = 0
1260
1261 if not options.fast:
1262 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001263 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001264 c = Changelist(branchref=b)
1265 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001266 props = {}
1267 r = None
1268 if i:
1269 try:
1270 props = c.GetIssueProperties()
1271 r = c.GetApprovingReviewers() if i else None
1272 except urllib2.HTTPError:
1273 # The issue probably doesn't exist anymore.
1274 i += ' (broken)'
1275
1276 msgs = props.get('messages') or []
1277
1278 if not i:
1279 color = Fore.WHITE
1280 elif props.get('closed'):
1281 # Issue is closed.
1282 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001283 elif props.get('commit'):
1284 # Issue is in the commit queue.
1285 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001286 elif r:
1287 # Was LGTM'ed.
1288 color = Fore.GREEN
1289 elif not msgs:
1290 # No message was sent.
1291 color = Fore.RED
1292 elif msgs[-1]['sender'] != props.get('owner_email'):
1293 color = Fore.YELLOW
1294 else:
1295 color = Fore.BLUE
1296 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001297
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001298 # Process one branch synchronously to work through authentication, then
1299 # spawn threads to process all the other branches in parallel.
1300 if branches:
1301 fetch(branches[0])
1302 threads = [
1303 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001304 for t in threads:
1305 t.daemon = True
1306 t.start()
1307 else:
1308 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1309 for b in branches:
1310 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001311 url = c.GetIssueURL()
1312 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001313
1314 tmp = {}
1315 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001316 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001317 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001318 b, i, color = output.get()
1319 tmp[b] = (i, color)
1320 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001321 reset = Fore.RESET
1322 if not sys.stdout.isatty():
1323 color = ''
1324 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001325 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001326 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001327
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001328 cl = Changelist()
1329 print
1330 print 'Current branch:',
1331 if not cl.GetIssue():
1332 print 'no issue assigned.'
1333 return 0
1334 print cl.GetBranch()
1335 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001336 if not options.fast:
1337 print 'Issue description:'
1338 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 return 0
1340
1341
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001342def colorize_CMDstatus_doc():
1343 """To be called once in main() to add colors to git cl status help."""
1344 colors = [i for i in dir(Fore) if i[0].isupper()]
1345
1346 def colorize_line(line):
1347 for color in colors:
1348 if color in line.upper():
1349 # Extract whitespaces first and the leading '-'.
1350 indent = len(line) - len(line.lstrip(' ')) + 1
1351 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1352 return line
1353
1354 lines = CMDstatus.__doc__.splitlines()
1355 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1356
1357
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001358@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001360 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
1362 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001363 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001364 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365
1366 cl = Changelist()
1367 if len(args) > 0:
1368 try:
1369 issue = int(args[0])
1370 except ValueError:
1371 DieWithError('Pass a number to set the issue or none to list it.\n'
1372 'Maybe you want to run git cl status?')
1373 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001374 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 return 0
1376
1377
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001378def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001379 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001380 (_, args) = parser.parse_args(args)
1381 if args:
1382 parser.error('Unsupported argument: %s' % args)
1383
1384 cl = Changelist()
1385 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001386 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001387 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001388 if message['disapproval']:
1389 color = Fore.RED
1390 elif message['approval']:
1391 color = Fore.GREEN
1392 elif message['sender'] == data['owner_email']:
1393 color = Fore.MAGENTA
1394 else:
1395 color = Fore.BLUE
1396 print '\n%s%s %s%s' % (
1397 color, message['date'].split('.', 1)[0], message['sender'],
1398 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001399 if message['text'].strip():
1400 print '\n'.join(' ' + l for l in message['text'].splitlines())
1401 return 0
1402
1403
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001404def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001405 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001406 cl = Changelist()
1407 if not cl.GetIssue():
1408 DieWithError('This branch has no associated changelist.')
1409 description = ChangeDescription(cl.GetDescription())
1410 description.prompt()
1411 cl.UpdateDescription(description.description)
1412 return 0
1413
1414
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415def CreateDescriptionFromLog(args):
1416 """Pulls out the commit log to use as a base for the CL description."""
1417 log_args = []
1418 if len(args) == 1 and not args[0].endswith('.'):
1419 log_args = [args[0] + '..']
1420 elif len(args) == 1 and args[0].endswith('...'):
1421 log_args = [args[0][:-1]]
1422 elif len(args) == 2:
1423 log_args = [args[0] + '..' + args[1]]
1424 else:
1425 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001426 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
1428
thestig@chromium.org44202a22014-03-11 19:22:18 +00001429def CMDlint(parser, args):
1430 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001431 parser.add_option('--filter', action='append', metavar='-x,+y',
1432 help='Comma-separated list of cpplint\'s category-filters')
1433 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001434
1435 # Access to a protected member _XX of a client class
1436 # pylint: disable=W0212
1437 try:
1438 import cpplint
1439 import cpplint_chromium
1440 except ImportError:
1441 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1442 return 1
1443
1444 # Change the current working directory before calling lint so that it
1445 # shows the correct base.
1446 previous_cwd = os.getcwd()
1447 os.chdir(settings.GetRoot())
1448 try:
1449 cl = Changelist()
1450 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1451 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001452 if not files:
1453 print "Cannot lint an empty CL"
1454 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001455
1456 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001457 command = args + files
1458 if options.filter:
1459 command = ['--filter=' + ','.join(options.filter)] + command
1460 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001461
1462 white_regex = re.compile(settings.GetLintRegex())
1463 black_regex = re.compile(settings.GetLintIgnoreRegex())
1464 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1465 for filename in filenames:
1466 if white_regex.match(filename):
1467 if black_regex.match(filename):
1468 print "Ignoring file %s" % filename
1469 else:
1470 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1471 extra_check_functions)
1472 else:
1473 print "Skipping file %s" % filename
1474 finally:
1475 os.chdir(previous_cwd)
1476 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1477 if cpplint._cpplint_state.error_count != 0:
1478 return 1
1479 return 0
1480
1481
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001483 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001484 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001486 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001487 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 (options, args) = parser.parse_args(args)
1489
ukai@chromium.org259e4682012-10-25 07:36:33 +00001490 if not options.force and is_dirty_git_tree('presubmit'):
1491 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 return 1
1493
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001494 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 if args:
1496 base_branch = args[0]
1497 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001498 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001499 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001501 cl.RunHook(
1502 committing=not options.upload,
1503 may_prompt=False,
1504 verbose=options.verbose,
1505 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001506 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507
1508
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001509def AddChangeIdToCommitMessage(options, args):
1510 """Re-commits using the current message, assumes the commit hook is in
1511 place.
1512 """
1513 log_desc = options.message or CreateDescriptionFromLog(args)
1514 git_command = ['commit', '--amend', '-m', log_desc]
1515 RunGit(git_command)
1516 new_log_desc = CreateDescriptionFromLog(args)
1517 if CHANGE_ID in new_log_desc:
1518 print 'git-cl: Added Change-Id to commit message.'
1519 else:
1520 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1521
1522
ukai@chromium.orge8077812012-02-03 03:41:46 +00001523def GerritUpload(options, args, cl):
1524 """upload the current branch to gerrit."""
1525 # We assume the remote called "origin" is the one we want.
1526 # It is probably not worthwhile to support different workflows.
1527 remote = 'origin'
1528 branch = 'master'
1529 if options.target_branch:
1530 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001532 change_desc = ChangeDescription(
1533 options.message or CreateDescriptionFromLog(args))
1534 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001535 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 if CHANGE_ID not in change_desc.description:
1538 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001539
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001540 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001541 if len(commits) > 1:
1542 print('WARNING: This will upload %d commits. Run the following command '
1543 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001544 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001545 print('You can also use `git squash-branch` to squash these into a single'
1546 'commit.')
1547 ask_for_data('About to upload; enter to confirm.')
1548
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001549 if options.reviewers:
1550 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551
ukai@chromium.orge8077812012-02-03 03:41:46 +00001552 receive_options = []
1553 cc = cl.GetCCList().split(',')
1554 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001555 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001556 cc = filter(None, cc)
1557 if cc:
1558 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001559 if change_desc.get_reviewers():
1560 receive_options.extend(
1561 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562
ukai@chromium.orge8077812012-02-03 03:41:46 +00001563 git_command = ['push']
1564 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001565 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001566 ' '.join(receive_options))
1567 git_command += [remote, 'HEAD:refs/for/' + branch]
1568 RunGit(git_command)
1569 # TODO(ukai): parse Change-Id: and set issue number?
1570 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001571
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572
jam@chromium.org82114c02014-08-22 22:13:21 +00001573def RietveldUpload(options, args, cl):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001574 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1576 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 if options.emulate_svn_auto_props:
1578 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579
1580 change_desc = None
1581
pgervais@chromium.org91141372014-01-09 23:27:20 +00001582 if options.email is not None:
1583 upload_args.extend(['--email', options.email])
1584
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001585 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001586 if options.title:
1587 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001588 if options.message:
1589 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001590 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591 print ("This branch is associated with issue %s. "
1592 "Adding patch to that issue." % cl.GetIssue())
1593 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001594 if options.title:
1595 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001596 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001597 change_desc = ChangeDescription(message)
1598 if options.reviewers:
1599 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001600 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001601 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001602
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001603 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 print "Description is empty; aborting."
1605 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001606
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001607 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001608 if change_desc.get_reviewers():
1609 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001610 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001611 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001612 DieWithError("Must specify reviewers to send email.")
1613 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001614
1615 # We check this before applying rietveld.private assuming that in
1616 # rietveld.cc only addresses which we can send private CLs to are listed
1617 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1618 # --private is specified explicitly on the command line.
1619 if options.private:
1620 logging.warn('rietveld.cc is ignored since private flag is specified. '
1621 'You need to review and add them manually if necessary.')
1622 cc = cl.GetCCListWithoutDefault()
1623 else:
1624 cc = cl.GetCCList()
1625 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001626 if cc:
1627 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001629 if options.private or settings.GetDefaultPrivateFlag() == "True":
1630 upload_args.append('--private')
1631
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001632 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001633 if not options.find_copies:
1634 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001635
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001636 # Include the upstream repo's URL in the change -- this is useful for
1637 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001638 remote_url = cl.GetGitBaseUrlFromConfig()
1639 if not remote_url:
1640 if settings.GetIsGitSvn():
1641 # URL is dependent on the current directory.
1642 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1643 if data:
1644 keys = dict(line.split(': ', 1) for line in data.splitlines()
1645 if ': ' in line)
1646 remote_url = keys.get('URL', None)
1647 else:
1648 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1649 remote_url = (cl.GetRemoteUrl() + '@'
1650 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 if remote_url:
1652 upload_args.extend(['--base_url', remote_url])
1653
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001654 project = settings.GetProject()
1655 if project:
1656 upload_args.extend(['--project', project])
1657
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001658 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001659 upload_args = ['upload'] + upload_args + args
1660 logging.info('upload.RealMain(%s)', upload_args)
1661 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001662 issue = int(issue)
1663 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001664 except KeyboardInterrupt:
1665 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001666 except:
1667 # If we got an exception after the user typed a description for their
1668 # change, back up the description before re-raising.
1669 if change_desc:
1670 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1671 print '\nGot exception while uploading -- saving description to %s\n' \
1672 % backup_path
1673 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001674 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675 backup_file.close()
1676 raise
1677
1678 if not cl.GetIssue():
1679 cl.SetIssue(issue)
1680 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001681
1682 if options.use_commit_queue:
1683 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001684 return 0
1685
1686
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001687def cleanup_list(l):
1688 """Fixes a list so that comma separated items are put as individual items.
1689
1690 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1691 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1692 """
1693 items = sum((i.split(',') for i in l), [])
1694 stripped_items = (i.strip() for i in items)
1695 return sorted(filter(None, stripped_items))
1696
1697
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001698@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001699def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001700 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001701 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1702 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001703 parser.add_option('--bypass-watchlists', action='store_true',
1704 dest='bypass_watchlists',
1705 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001706 parser.add_option('-f', action='store_true', dest='force',
1707 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001708 parser.add_option('-m', dest='message', help='message for patchset')
1709 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001710 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001711 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001712 help='reviewer email addresses')
1713 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001714 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001715 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001716 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001717 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001718 parser.add_option('--emulate_svn_auto_props',
1719 '--emulate-svn-auto-props',
1720 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001721 dest="emulate_svn_auto_props",
1722 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001723 parser.add_option('-c', '--use-commit-queue', action='store_true',
1724 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001725 parser.add_option('--private', action='store_true',
1726 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001727 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001728 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001729 help='When uploading to gerrit, remote branch to '
1730 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001731 parser.add_option('--email', default=None,
1732 help='email address to use to connect to Rietveld')
1733
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001734 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001735 (options, args) = parser.parse_args(args)
1736
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001737 if options.target_branch and not settings.GetIsGerrit():
1738 parser.error('Use --target_branch for non gerrit repository.')
1739
ukai@chromium.org259e4682012-10-25 07:36:33 +00001740 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001741 return 1
1742
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001743 options.reviewers = cleanup_list(options.reviewers)
1744 options.cc = cleanup_list(options.cc)
1745
ukai@chromium.orge8077812012-02-03 03:41:46 +00001746 cl = Changelist()
1747 if args:
1748 # TODO(ukai): is it ok for gerrit case?
1749 base_branch = args[0]
1750 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001751 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001752 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001753 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001754
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001755 # Apply watchlists on upload.
1756 change = cl.GetChange(base_branch, None)
1757 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1758 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001759 if not options.bypass_watchlists:
1760 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001761
ukai@chromium.orge8077812012-02-03 03:41:46 +00001762 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001763 if options.reviewers:
1764 # Set the reviewer list now so that presubmit checks can access it.
1765 change_description = ChangeDescription(change.FullDescriptionText())
1766 change_description.update_reviewers(options.reviewers)
1767 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001768 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001769 may_prompt=not options.force,
1770 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001771 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001772 if not hook_results.should_continue():
1773 return 1
1774 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001775 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001776
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001777 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001778 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001779 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001780 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001781 print ('The last upload made from this repository was patchset #%d but '
1782 'the most recent patchset on the server is #%d.'
1783 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001784 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1785 'from another machine or branch the patch you\'re uploading now '
1786 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001787 ask_for_data('About to upload; enter to confirm.')
1788
iannucci@chromium.org79540052012-10-19 23:15:26 +00001789 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001790 if settings.GetIsGerrit():
1791 return GerritUpload(options, args, cl)
jam@chromium.org82114c02014-08-22 22:13:21 +00001792 ret = RietveldUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001793 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001794 git_set_branch_value('last-upload-hash',
1795 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001796
1797 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001798
1799
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001800def IsSubmoduleMergeCommit(ref):
1801 # When submodules are added to the repo, we expect there to be a single
1802 # non-git-svn merge commit at remote HEAD with a signature comment.
1803 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001804 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001805 return RunGit(cmd) != ''
1806
1807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001809 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001811 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812 Updates changelog with metadata (e.g. pointer to review).
1813 Pushes/dcommits the code upstream.
1814 Updates review and closes.
1815 """
1816 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1817 help='bypass upload presubmit hook')
1818 parser.add_option('-m', dest='message',
1819 help="override review description")
1820 parser.add_option('-f', action='store_true', dest='force',
1821 help="force yes to questions (don't prompt)")
1822 parser.add_option('-c', dest='contributor',
1823 help="external contributor for patch (appended to " +
1824 "description and used as author for git). Should be " +
1825 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001826 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827 (options, args) = parser.parse_args(args)
1828 cl = Changelist()
1829
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001830 current = cl.GetBranch()
1831 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1832 if not settings.GetIsGitSvn() and remote == '.':
1833 print
1834 print 'Attempting to push branch %r into another local branch!' % current
1835 print
1836 print 'Either reparent this branch on top of origin/master:'
1837 print ' git reparent-branch --root'
1838 print
1839 print 'OR run `git rebase-update` if you think the parent branch is already'
1840 print 'committed.'
1841 print
1842 print ' Current parent: %r' % upstream_branch
1843 return 1
1844
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001845 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 # Default to merging against our best guess of the upstream branch.
1847 args = [cl.GetUpstreamBranch()]
1848
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001849 if options.contributor:
1850 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1851 print "Please provide contibutor as 'First Last <email@example.com>'"
1852 return 1
1853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001855 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001856
ukai@chromium.org259e4682012-10-25 07:36:33 +00001857 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001858 return 1
1859
1860 # This rev-list syntax means "show all commits not in my branch that
1861 # are in base_branch".
1862 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1863 base_branch]).splitlines()
1864 if upstream_commits:
1865 print ('Base branch "%s" has %d commits '
1866 'not in this branch.' % (base_branch, len(upstream_commits)))
1867 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1868 return 1
1869
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001870 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001871 svn_head = None
1872 if cmd == 'dcommit' or base_has_submodules:
1873 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1874 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001875
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001877 # If the base_head is a submodule merge commit, the first parent of the
1878 # base_head should be a git-svn commit, which is what we're interested in.
1879 base_svn_head = base_branch
1880 if base_has_submodules:
1881 base_svn_head += '^1'
1882
1883 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884 if extra_commits:
1885 print ('This branch has %d additional commits not upstreamed yet.'
1886 % len(extra_commits.splitlines()))
1887 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1888 'before attempting to %s.' % (base_branch, cmd))
1889 return 1
1890
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001891 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001892 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001893 author = None
1894 if options.contributor:
1895 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001896 hook_results = cl.RunHook(
1897 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001898 may_prompt=not options.force,
1899 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001900 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001901 if not hook_results.should_continue():
1902 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001904 # Check the tree status if the tree status URL is set.
1905 status = GetTreeStatus()
1906 if 'closed' == status:
1907 print('The tree is closed. Please wait for it to reopen. Use '
1908 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1909 return 1
1910 elif 'unknown' == status:
1911 print('Unable to determine tree status. Please verify manually and '
1912 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1913 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00001914 else:
1915 breakpad.SendStack(
1916 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001917 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1918 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001919 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001921 change_desc = ChangeDescription(options.message)
1922 if not change_desc.description and cl.GetIssue():
1923 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001925 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001926 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001927 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001928 else:
1929 print 'No description set.'
1930 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1931 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001933 # Keep a separate copy for the commit message, because the commit message
1934 # contains the link to the Rietveld issue, while the Rietveld message contains
1935 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001936 # Keep a separate copy for the commit message.
1937 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001938 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001939
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001940 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001941 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001942 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001944 commit_desc.append_footer('Patch from %s.' % options.contributor)
1945
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001946 print('Description:')
1947 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948
1949 branches = [base_branch, cl.GetBranchRef()]
1950 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001951 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001953 # We want to squash all this branch's commits into one commit with the proper
1954 # description. We do this by doing a "reset --soft" to the base branch (which
1955 # keeps the working copy the same), then dcommitting that. If origin/master
1956 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1957 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001959 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1960 # Delete the branches if they exist.
1961 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1962 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1963 result = RunGitWithCode(showref_cmd)
1964 if result[0] == 0:
1965 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966
1967 # We might be in a directory that's present in this branch but not in the
1968 # trunk. Move up to the top of the tree so that git commands that expect a
1969 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001970 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971 if rel_base_path:
1972 os.chdir(rel_base_path)
1973
1974 # Stuff our change into the merge branch.
1975 # We wrap in a try...finally block so if anything goes wrong,
1976 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001977 retcode = -1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001978 used_pending = False
1979 pending_ref = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001980 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001981 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1982 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001984 RunGit(
1985 [
1986 'commit', '--author', options.contributor,
1987 '-m', commit_desc.description,
1988 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001990 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001991 if base_has_submodules:
1992 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1993 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1994 RunGit(['checkout', CHERRY_PICK_BRANCH])
1995 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001996 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001997 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001998 pending_prefix = settings.GetPendingRefPrefix()
1999 if not pending_prefix or branch.startswith(pending_prefix):
2000 # If not using refs/pending/heads/* at all, or target ref is already set
2001 # to pending, then push to the target ref directly.
2002 retcode, output = RunGitWithCode(
2003 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
2004 used_pending = pending_prefix and branch.startswith(pending_prefix)
2005 else:
2006 # Cherry-pick the change on top of pending ref and then push it.
2007 assert branch.startswith('refs/'), branch
2008 assert pending_prefix[-1] == '/', pending_prefix
2009 pending_ref = pending_prefix + branch[len('refs/'):]
2010 retcode, output = PushToGitPending(remote, pending_ref, branch)
2011 used_pending = (retcode == 0)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002012 logging.debug(output)
2013 else:
2014 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002015 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002016 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002017 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018 finally:
2019 # And then swap back to the original branch and clean up.
2020 RunGit(['checkout', '-q', cl.GetBranch()])
2021 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002022 if base_has_submodules:
2023 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024
2025 if cl.GetIssue():
2026 if cmd == 'dcommit' and 'Committed r' in output:
2027 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002028 elif cmd == 'land' and retcode == 0:
mark@chromium.org671c7a32014-07-31 17:09:36 +00002029 match = (re.match(r'.*?([a-f0-9]{7,})\.\.([a-f0-9]{7,})$', l)
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00002030 for l in output.splitlines(False))
2031 match = filter(None, match)
2032 if len(match) != 1:
2033 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
2034 output)
2035 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036 else:
2037 return 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002038 to_pending = ' to pending queue' if used_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039 viewvc_url = settings.GetViewVCUrl()
2040 if viewvc_url and revision:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002041 change_desc.append_footer(
2042 'Committed%s: %s%s' % (to_pending, viewvc_url, revision))
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00002043 elif revision:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002044 change_desc.append_footer('Committed%s: %s' % (to_pending, revision))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045 print ('Closing issue '
2046 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002047 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002049 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002050 patch_num = len(props['patchsets'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002051 comment = "Committed patchset #%d%s manually as %s" % (
2052 patch_num, to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002053 if options.bypass_hooks:
2054 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2055 else:
2056 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002057 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002058 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002059
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002060 if used_pending and retcode == 0:
2061 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2062 print 'The commit is in the pending queue (%s).' % pending_ref
2063 print (
2064 'It will show up on %s in ~1 min, once it gets Cr-Commit-Position '
2065 'footer.' % branch)
2066
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002067 if retcode == 0:
2068 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2069 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002070 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002071
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072 return 0
2073
2074
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002075def PushToGitPending(remote, pending_ref, upstream_ref):
2076 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2077
2078 Returns:
2079 (retcode of last operation, output log of last operation).
2080 """
2081 assert pending_ref.startswith('refs/'), pending_ref
2082 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2083 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2084 code = 0
2085 out = ''
2086 attempt = 0
2087 while attempt < 5:
2088 attempt += 1
2089
2090 # Fetch. Retry fetch errors.
2091 code, out = RunGitWithCode(
2092 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)],
2093 suppress_stderr=True)
2094 if code:
2095 continue
2096
2097 # Try to cherry pick. Abort on merge conflicts.
2098 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
2099 code, out = RunGitWithCode(['cherry-pick', cherry], suppress_stderr=True)
2100 if code:
2101 print (
2102 'Your patch doesn\'t apply cleanly to upstream ref \'%s\', '
2103 'the following files have merge conflicts:' % upstream_ref)
2104 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2105 print 'Please rebase your patch and try again.'
2106 RunGitWithCode(['cherry-pick', '--abort'], suppress_stderr=True)
2107 return code, out
2108
2109 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
2110 code, out = RunGitWithCode(
2111 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2112 if code == 0:
2113 # Success.
2114 return code, out
2115
2116 return code, out
2117
2118
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002119@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002121 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002123 message = """This doesn't appear to be an SVN repository.
2124If your project has a git mirror with an upstream SVN master, you probably need
2125to run 'git svn init', see your project's git mirror documentation.
2126If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002127to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002128Choose wisely, if you get this wrong, your commit might appear to succeed but
2129will instead be silently ignored."""
2130 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002131 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002132 return SendUpstream(parser, args, 'dcommit')
2133
2134
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002135@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002136def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002137 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 if settings.GetIsGitSvn():
2139 print('This appears to be an SVN repository.')
2140 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002141 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002142 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002143
2144
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002145@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002146def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002147 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 parser.add_option('-b', dest='newbranch',
2149 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002150 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002152 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2153 help='Change to the directory DIR immediately, '
2154 'before doing anything else.')
2155 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002156 help='failed patches spew .rej files rather than '
2157 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2159 help="don't commit after patch applies")
2160 (options, args) = parser.parse_args(args)
2161 if len(args) != 1:
2162 parser.print_help()
2163 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002164 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002166 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002167 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002168
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002169 if options.newbranch:
2170 if options.force:
2171 RunGit(['branch', '-D', options.newbranch],
2172 stderr=subprocess2.PIPE, error_ok=True)
2173 RunGit(['checkout', '-b', options.newbranch,
2174 Changelist().GetUpstreamBranch()])
2175
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002176 return PatchIssue(issue_arg, options.reject, options.nocommit,
2177 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002178
2179
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002180def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002181 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002182 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002183 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002184 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002185 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002186 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002188 # Assume it's a URL to the patch. Default to https.
2189 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002190 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002191 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192 DieWithError('Must pass an issue ID or full URL for '
2193 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002194 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002195 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002196 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002198 # Switch up to the top-level directory, if necessary, in preparation for
2199 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002200 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201 if top:
2202 os.chdir(top)
2203
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002204 # Git patches have a/ at the beginning of source paths. We strip that out
2205 # with a sed script rather than the -p flag to patch so we can feed either
2206 # Git or svn-style patches into the same apply command.
2207 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002208 try:
2209 patch_data = subprocess2.check_output(
2210 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2211 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002212 DieWithError('Git patch mungling failed.')
2213 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002214
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002215 # We use "git apply" to apply the patch instead of "patch" so that we can
2216 # pick up file adds.
2217 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002218 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002219 if directory:
2220 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002221 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002222 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002223 elif IsGitVersionAtLeast('1.7.12'):
2224 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002225 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002226 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002227 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002228 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002229 DieWithError('Failed to apply the patch')
2230
2231 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002232 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002233 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2234 cl = Changelist()
2235 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002236 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002237 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002238 else:
2239 print "Patch applied to index."
2240 return 0
2241
2242
2243def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002244 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002245 # Provide a wrapper for git svn rebase to help avoid accidental
2246 # git svn dcommit.
2247 # It's the only command that doesn't use parser at all since we just defer
2248 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002249
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002250 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002251
2252
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002253def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002254 """Fetches the tree status and returns either 'open', 'closed',
2255 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002256 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002257 if url:
2258 status = urllib2.urlopen(url).read().lower()
2259 if status.find('closed') != -1 or status == '0':
2260 return 'closed'
2261 elif status.find('open') != -1 or status == '1':
2262 return 'open'
2263 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002264 return 'unset'
2265
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002267def GetTreeStatusReason():
2268 """Fetches the tree status from a json url and returns the message
2269 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002270 url = settings.GetTreeStatusUrl()
2271 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002272 connection = urllib2.urlopen(json_url)
2273 status = json.loads(connection.read())
2274 connection.close()
2275 return status['message']
2276
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002277
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002278def GetBuilderMaster(bot_list):
2279 """For a given builder, fetch the master from AE if available."""
2280 map_url = 'https://builders-map.appspot.com/'
2281 try:
2282 master_map = json.load(urllib2.urlopen(map_url))
2283 except urllib2.URLError as e:
2284 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2285 (map_url, e))
2286 except ValueError as e:
2287 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2288 if not master_map:
2289 return None, 'Failed to build master map.'
2290
2291 result_master = ''
2292 for bot in bot_list:
2293 builder = bot.split(':', 1)[0]
2294 master_list = master_map.get(builder, [])
2295 if not master_list:
2296 return None, ('No matching master for builder %s.' % builder)
2297 elif len(master_list) > 1:
2298 return None, ('The builder name %s exists in multiple masters %s.' %
2299 (builder, master_list))
2300 else:
2301 cur_master = master_list[0]
2302 if not result_master:
2303 result_master = cur_master
2304 elif result_master != cur_master:
2305 return None, 'The builders do not belong to the same master.'
2306 return result_master, None
2307
2308
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002310 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002311 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002312 status = GetTreeStatus()
2313 if 'unset' == status:
2314 print 'You must configure your tree status URL by running "git cl config".'
2315 return 2
2316
2317 print "The tree is %s" % status
2318 print
2319 print GetTreeStatusReason()
2320 if status != 'open':
2321 return 1
2322 return 0
2323
2324
maruel@chromium.org15192402012-09-06 12:38:29 +00002325def CMDtry(parser, args):
2326 """Triggers a try job through Rietveld."""
2327 group = optparse.OptionGroup(parser, "Try job options")
2328 group.add_option(
2329 "-b", "--bot", action="append",
2330 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2331 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002332 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002333 "the try server waterfall for the builders name and the tests "
2334 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002335 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002336 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002337 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002338 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002339 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002340 "-r", "--revision",
2341 help="Revision to use for the try job; default: the "
2342 "revision will be determined by the try server; see "
2343 "its waterfall for more info")
2344 group.add_option(
2345 "-c", "--clobber", action="store_true", default=False,
2346 help="Force a clobber before building; e.g. don't do an "
2347 "incremental build")
2348 group.add_option(
2349 "--project",
2350 help="Override which project to use. Projects are defined "
2351 "server-side to define what default bot set to use")
2352 group.add_option(
2353 "-t", "--testfilter", action="append", default=[],
2354 help=("Apply a testfilter to all the selected builders. Unless the "
2355 "builders configurations are similar, use multiple "
2356 "--bot <builder>:<test> arguments."))
2357 group.add_option(
2358 "-n", "--name", help="Try job name; default to current branch name")
2359 parser.add_option_group(group)
2360 options, args = parser.parse_args(args)
2361
2362 if args:
2363 parser.error('Unknown arguments: %s' % args)
2364
2365 cl = Changelist()
2366 if not cl.GetIssue():
2367 parser.error('Need to upload first')
2368
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002369 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002370 if props.get('closed'):
2371 parser.error('Cannot send tryjobs for a closed CL')
2372
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002373 if props.get('private'):
2374 parser.error('Cannot use trybots with private issue')
2375
maruel@chromium.org15192402012-09-06 12:38:29 +00002376 if not options.name:
2377 options.name = cl.GetBranch()
2378
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002379 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002380 options.master, err_msg = GetBuilderMaster(options.bot)
2381 if err_msg:
2382 parser.error('Tryserver master cannot be found because: %s\n'
2383 'Please manually specify the tryserver master'
2384 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002385
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002386 def GetMasterMap():
2387 # Process --bot and --testfilter.
2388 if not options.bot:
2389 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002390
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002391 # Get try masters from PRESUBMIT.py files.
2392 masters = presubmit_support.DoGetTryMasters(
2393 change,
2394 change.LocalPaths(),
2395 settings.GetRoot(),
2396 None,
2397 None,
2398 options.verbose,
2399 sys.stdout)
2400 if masters:
2401 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002402
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002403 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2404 options.bot = presubmit_support.DoGetTrySlaves(
2405 change,
2406 change.LocalPaths(),
2407 settings.GetRoot(),
2408 None,
2409 None,
2410 options.verbose,
2411 sys.stdout)
2412 if not options.bot:
2413 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002414
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002415 builders_and_tests = {}
2416 # TODO(machenbach): The old style command-line options don't support
2417 # multiple try masters yet.
2418 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2419 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2420
2421 for bot in old_style:
2422 if ':' in bot:
2423 builder, tests = bot.split(':', 1)
2424 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2425 elif ',' in bot:
2426 parser.error('Specify one bot per --bot flag')
2427 else:
2428 builders_and_tests.setdefault(bot, []).append('defaulttests')
2429
2430 for bot, tests in new_style:
2431 builders_and_tests.setdefault(bot, []).extend(tests)
2432
2433 # Return a master map with one master to be backwards compatible. The
2434 # master name defaults to an empty string, which will cause the master
2435 # not to be set on rietveld (deprecated).
2436 return {options.master: builders_and_tests}
2437
2438 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002439
maruel@chromium.org15192402012-09-06 12:38:29 +00002440 if options.testfilter:
2441 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002442 masters = dict((master, dict(
2443 (b, forced_tests) for b, t in slaves.iteritems()
2444 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002445
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002446 for builders in masters.itervalues():
2447 if any('triggered' in b for b in builders):
2448 print >> sys.stderr, (
2449 'ERROR You are trying to send a job to a triggered bot. This type of'
2450 ' bot requires an\ninitial job from a parent (usually a builder). '
2451 'Instead send your job to the parent.\n'
2452 'Bot list: %s' % builders)
2453 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002454
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002455 patchset = cl.GetMostRecentPatchset()
2456 if patchset and patchset != cl.GetPatchset():
2457 print(
2458 '\nWARNING Mismatch between local config and server. Did a previous '
2459 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2460 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002461 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002462 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002463 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002464 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002465 except urllib2.HTTPError, e:
2466 if e.code == 404:
2467 print('404 from rietveld; '
2468 'did you mean to use "git try" instead of "git cl try"?')
2469 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002470 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002471
2472 for (master, builders) in masters.iteritems():
2473 if master:
2474 print 'Master: %s' % master
2475 length = max(len(builder) for builder in builders)
2476 for builder in sorted(builders):
2477 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002478 return 0
2479
2480
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002481@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002482def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002483 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002484 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002485 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002486 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002487
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002488 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002489 if args:
2490 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002491 branch = cl.GetBranch()
2492 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002493 cl = Changelist()
2494 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002495
2496 # Clear configured merge-base, if there is one.
2497 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002498 else:
2499 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002500 return 0
2501
2502
thestig@chromium.org00858c82013-12-02 23:08:03 +00002503def CMDweb(parser, args):
2504 """Opens the current CL in the web browser."""
2505 _, args = parser.parse_args(args)
2506 if args:
2507 parser.error('Unrecognized args: %s' % ' '.join(args))
2508
2509 issue_url = Changelist().GetIssueURL()
2510 if not issue_url:
2511 print >> sys.stderr, 'ERROR No issue to open'
2512 return 1
2513
2514 webbrowser.open(issue_url)
2515 return 0
2516
2517
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002518def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002519 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002520 _, args = parser.parse_args(args)
2521 if args:
2522 parser.error('Unrecognized args: %s' % ' '.join(args))
2523 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002524 props = cl.GetIssueProperties()
2525 if props.get('private'):
2526 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002527 cl.SetFlag('commit', '1')
2528 return 0
2529
2530
groby@chromium.org411034a2013-02-26 15:12:01 +00002531def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002532 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002533 _, args = parser.parse_args(args)
2534 if args:
2535 parser.error('Unrecognized args: %s' % ' '.join(args))
2536 cl = Changelist()
2537 # Ensure there actually is an issue to close.
2538 cl.GetDescription()
2539 cl.CloseIssue()
2540 return 0
2541
2542
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002543def CMDdiff(parser, args):
2544 """shows differences between local tree and last upload."""
2545 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002546 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002547 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002548 if not issue:
2549 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002550 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002551 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002552
2553 # Create a new branch based on the merge-base
2554 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2555 try:
2556 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002557 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002558 if rtn != 0:
2559 return rtn
2560
2561 # Switch back to starting brand and diff against the temporary
2562 # branch containing the latest rietveld patch.
2563 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2564 finally:
2565 RunGit(['checkout', '-q', branch])
2566 RunGit(['branch', '-D', TMP_BRANCH])
2567
2568 return 0
2569
2570
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002571def CMDowners(parser, args):
2572 """interactively find the owners for reviewing"""
2573 parser.add_option(
2574 '--no-color',
2575 action='store_true',
2576 help='Use this option to disable color output')
2577 options, args = parser.parse_args(args)
2578
2579 author = RunGit(['config', 'user.email']).strip() or None
2580
2581 cl = Changelist()
2582
2583 if args:
2584 if len(args) > 1:
2585 parser.error('Unknown args')
2586 base_branch = args[0]
2587 else:
2588 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002589 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002590
2591 change = cl.GetChange(base_branch, None)
2592 return owners_finder.OwnersFinder(
2593 [f.LocalPath() for f in
2594 cl.GetChange(base_branch, None).AffectedFiles()],
2595 change.RepositoryRoot(), author,
2596 fopen=file, os_path=os.path, glob=glob.glob,
2597 disable_color=options.no_color).run()
2598
2599
enne@chromium.org555cfe42014-01-29 18:21:39 +00002600@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002601def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002602 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002603 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002604 parser.add_option('--full', action='store_true',
2605 help='Reformat the full content of all touched files')
2606 parser.add_option('--dry-run', action='store_true',
2607 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002608 parser.add_option('--diff', action='store_true',
2609 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002610 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002611
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002612 # git diff generates paths against the root of the repository. Change
2613 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002614 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002615 if rel_base_path:
2616 os.chdir(rel_base_path)
2617
digit@chromium.org29e47272013-05-17 17:01:46 +00002618 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002619 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002620 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002621 # Only list the names of modified files.
2622 diff_cmd.append('--name-only')
2623 else:
2624 # Only generate context-less patches.
2625 diff_cmd.append('-U0')
2626
2627 # Grab the merge-base commit, i.e. the upstream commit of the current
2628 # branch when it was created or the last time it was rebased. This is
2629 # to cover the case where the user may have called "git fetch origin",
2630 # moving the origin branch to a newer commit, but hasn't rebased yet.
2631 upstream_commit = None
2632 cl = Changelist()
2633 upstream_branch = cl.GetUpstreamBranch()
2634 if upstream_branch:
2635 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2636 upstream_commit = upstream_commit.strip()
2637
2638 if not upstream_commit:
2639 DieWithError('Could not find base commit for this branch. '
2640 'Are you in detached state?')
2641
2642 diff_cmd.append(upstream_commit)
2643
2644 # Handle source file filtering.
2645 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002646 if args:
2647 for arg in args:
2648 if os.path.isdir(arg):
2649 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2650 elif os.path.isfile(arg):
2651 diff_cmd.append(arg)
2652 else:
2653 DieWithError('Argument "%s" is not a file or a directory' % arg)
2654 else:
2655 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002656 diff_output = RunGit(diff_cmd)
2657
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002658 top_dir = os.path.normpath(
2659 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2660
2661 # Locate the clang-format binary in the checkout
2662 try:
2663 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2664 except clang_format.NotFoundError, e:
2665 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002666
digit@chromium.org29e47272013-05-17 17:01:46 +00002667 if opts.full:
2668 # diff_output is a list of files to send to clang-format.
2669 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002670 if not files:
2671 print "Nothing to format."
2672 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002673 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002674 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002675 cmd.append('-i')
2676 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002677 if opts.diff:
2678 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002679 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002680 env = os.environ.copy()
2681 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002682 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002683 try:
2684 script = clang_format.FindClangFormatScriptInChromiumTree(
2685 'clang-format-diff.py')
2686 except clang_format.NotFoundError, e:
2687 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002688
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002689 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002690 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002691 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002692
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002693 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002694 if opts.diff:
2695 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002696 if opts.dry_run and len(stdout) > 0:
2697 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002698
2699 return 0
2700
2701
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002702class OptionParser(optparse.OptionParser):
2703 """Creates the option parse and add --verbose support."""
2704 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002705 optparse.OptionParser.__init__(
2706 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002707 self.add_option(
2708 '-v', '--verbose', action='count', default=0,
2709 help='Use 2 times for more debugging info')
2710
2711 def parse_args(self, args=None, values=None):
2712 options, args = optparse.OptionParser.parse_args(self, args, values)
2713 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2714 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2715 return options, args
2716
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002718def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002719 if sys.hexversion < 0x02060000:
2720 print >> sys.stderr, (
2721 '\nYour python version %s is unsupported, please upgrade.\n' %
2722 sys.version.split(' ', 1)[0])
2723 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002724
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002725 # Reload settings.
2726 global settings
2727 settings = Settings()
2728
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002729 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002730 dispatcher = subcommand.CommandDispatcher(__name__)
2731 try:
2732 return dispatcher.execute(OptionParser(), argv)
2733 except urllib2.HTTPError, e:
2734 if e.code != 500:
2735 raise
2736 DieWithError(
2737 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2738 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002739
2740
2741if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002742 # These affect sys.stdout so do it outside of main() to simplify mocks in
2743 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002744 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002745 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746 sys.exit(main(sys.argv[1:]))