blob: 72770a145e4194197b910b7c0eb7db5cb2469e08 [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
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000010import datetime
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000013import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import logging
15import optparse
16import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000017import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import textwrap
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000022import time
maruel@chromium.org1033efd2013-07-23 23:25:09 +000023import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000025import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000026import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027
28try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000029 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030except ImportError:
31 pass
32
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035from third_party import upload
36import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
41import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000043import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000046import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047import watchlists
48
maruel@chromium.org0633fb42013-08-16 20:06:14 +000049__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000051DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000052POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000054GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000055CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056
thestig@chromium.org44202a22014-03-11 19:22:18 +000057# Valid extensions for files we want to lint.
58DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
59DEFAULT_LINT_IGNORE_REGEX = r"$^"
60
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000061# Shortcut since it quickly becomes redundant.
62Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000063
maruel@chromium.orgddd59412011-11-30 14:20:38 +000064# Initialized in main()
65settings = None
66
67
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000069 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070 sys.exit(1)
71
72
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000073def GetNoGitPagerEnv():
74 env = os.environ.copy()
75 # 'cat' is a magical git string that disables pagers on all platforms.
76 env['GIT_PAGER'] = 'cat'
77 return env
78
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000081 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000082 except subprocess2.CalledProcessError as e:
83 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000086 'Command "%s" failed.\n%s' % (
87 ' '.join(args), error_message or e.stdout or ''))
88 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089
90
91def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000092 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000093 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000094
95
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000096def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000098 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000099 if suppress_stderr:
100 stderr = subprocess2.VOID
101 else:
102 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000103 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000104 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000105 stdout=subprocess2.PIPE,
106 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000107 return code, out[0]
108 except ValueError:
109 # When the subprocess fails, it returns None. That triggers a ValueError
110 # when trying to unpack the return value into (out, code).
111 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000114def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000115 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000117 return (version.startswith(prefix) and
118 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000119
120
maruel@chromium.org90541732011-04-01 17:54:18 +0000121def ask_for_data(prompt):
122 try:
123 return raw_input(prompt)
124 except KeyboardInterrupt:
125 # Hide the exception.
126 sys.exit(1)
127
128
iannucci@chromium.org79540052012-10-19 23:15:26 +0000129def git_set_branch_value(key, value):
130 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000131 if not branch:
132 return
133
134 cmd = ['config']
135 if isinstance(value, int):
136 cmd.append('--int')
137 git_key = 'branch.%s.%s' % (branch, key)
138 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139
140
141def git_get_branch_default(key, default):
142 branch = Changelist().GetBranch()
143 if branch:
144 git_key = 'branch.%s.%s' % (branch, key)
145 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
146 try:
147 return int(stdout.strip())
148 except ValueError:
149 pass
150 return default
151
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153def add_git_similarity(parser):
154 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 help='Sets the percentage that a pair of files need to match in order to'
157 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158 parser.add_option(
159 '--find-copies', action='store_true',
160 help='Allows git to look for copies.')
161 parser.add_option(
162 '--no-find-copies', action='store_false', dest='find_copies',
163 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164
165 old_parser_args = parser.parse_args
166 def Parse(args):
167 options, args = old_parser_args(args)
168
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000171 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 print('Note: Saving similarity of %d%% in git config.'
173 % options.similarity)
174 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000175
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 options.similarity = max(0, min(options.similarity, 100))
177
178 if options.find_copies is None:
179 options.find_copies = bool(
180 git_get_branch_default('git-find-copies', True))
181 else:
182 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183
184 print('Using %d%% similarity for rename/copy detection. '
185 'Override with --similarity.' % options.similarity)
186
187 return options, args
188 parser.parse_args = Parse
189
190
ukai@chromium.org259e4682012-10-25 07:36:33 +0000191def is_dirty_git_tree(cmd):
192 # Make sure index is up-to-date before running diff-index.
193 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
194 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
195 if dirty:
196 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
197 print 'Uncommitted files: (git diff-index --name-status HEAD)'
198 print dirty[:4096]
199 if len(dirty) > 4096:
200 print '... (run "git diff-index --name-status HEAD" to see full output).'
201 return True
202 return False
203
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000204
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000205def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
206 """Return the corresponding git ref if |base_url| together with |glob_spec|
207 matches the full |url|.
208
209 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
210 """
211 fetch_suburl, as_ref = glob_spec.split(':')
212 if allow_wildcards:
213 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
214 if glob_match:
215 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
216 # "branches/{472,597,648}/src:refs/remotes/svn/*".
217 branch_re = re.escape(base_url)
218 if glob_match.group(1):
219 branch_re += '/' + re.escape(glob_match.group(1))
220 wildcard = glob_match.group(2)
221 if wildcard == '*':
222 branch_re += '([^/]*)'
223 else:
224 # Escape and replace surrounding braces with parentheses and commas
225 # with pipe symbols.
226 wildcard = re.escape(wildcard)
227 wildcard = re.sub('^\\\\{', '(', wildcard)
228 wildcard = re.sub('\\\\,', '|', wildcard)
229 wildcard = re.sub('\\\\}$', ')', wildcard)
230 branch_re += wildcard
231 if glob_match.group(3):
232 branch_re += re.escape(glob_match.group(3))
233 match = re.match(branch_re, url)
234 if match:
235 return re.sub('\*$', match.group(1), as_ref)
236
237 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
238 if fetch_suburl:
239 full_url = base_url + '/' + fetch_suburl
240 else:
241 full_url = base_url
242 if full_url == url:
243 return as_ref
244 return None
245
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000246
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 """Prints statistics about the change to the user."""
249 # --no-ext-diff is broken in some versions of Git, so try to work around
250 # this by overriding the environment (but there is still a problem if the
251 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000252 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000253 if 'GIT_EXTERNAL_DIFF' in env:
254 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255
256 if find_copies:
257 similarity_options = ['--find-copies-harder', '-l100000',
258 '-C%s' % similarity]
259 else:
260 similarity_options = ['-M%s' % similarity]
261
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000262 try:
263 stdout = sys.stdout.fileno()
264 except AttributeError:
265 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000266 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000267 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000268 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000269 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000270
271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272class Settings(object):
273 def __init__(self):
274 self.default_server = None
275 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000276 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 self.is_git_svn = None
278 self.svn_branch = None
279 self.tree_status_url = None
280 self.viewvc_url = None
281 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000282 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000283 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000284 self.project = 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:
bratell@opera.com05fb9112014-07-07 09:30:23 +0000330 # If you have any "svn-remote.*" config keys, we think you're using svn.
331 self.is_git_svn = RunGitWithCode(
332 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333 return self.is_git_svn
334
335 def GetSVNBranch(self):
336 if self.svn_branch is None:
337 if not self.GetIsGitSvn():
338 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
339
340 # Try to figure out which remote branch we're based on.
341 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000342 # 1) iterate through our branch history and find the svn URL.
343 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000344
345 # regexp matching the git-svn line that contains the URL.
346 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
347
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000348 # We don't want to go through all of history, so read a line from the
349 # pipe at a time.
350 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000351 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000352 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
353 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000354 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000355 for line in proc.stdout:
356 match = git_svn_re.match(line)
357 if match:
358 url = match.group(1)
359 proc.stdout.close() # Cut pipe.
360 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000361
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 if url:
363 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
364 remotes = RunGit(['config', '--get-regexp',
365 r'^svn-remote\..*\.url']).splitlines()
366 for remote in remotes:
367 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000368 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000369 remote = match.group(1)
370 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000371 rewrite_root = RunGit(
372 ['config', 'svn-remote.%s.rewriteRoot' % remote],
373 error_ok=True).strip()
374 if rewrite_root:
375 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000376 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000377 ['config', 'svn-remote.%s.fetch' % remote],
378 error_ok=True).strip()
379 if fetch_spec:
380 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
381 if self.svn_branch:
382 break
383 branch_spec = RunGit(
384 ['config', 'svn-remote.%s.branches' % remote],
385 error_ok=True).strip()
386 if branch_spec:
387 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
388 if self.svn_branch:
389 break
390 tag_spec = RunGit(
391 ['config', 'svn-remote.%s.tags' % remote],
392 error_ok=True).strip()
393 if tag_spec:
394 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
395 if self.svn_branch:
396 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000397
398 if not self.svn_branch:
399 DieWithError('Can\'t guess svn branch -- try specifying it on the '
400 'command line')
401
402 return self.svn_branch
403
404 def GetTreeStatusUrl(self, error_ok=False):
405 if not self.tree_status_url:
406 error_message = ('You must configure your tree status URL by running '
407 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000408 self.tree_status_url = self._GetRietveldConfig(
409 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000410 return self.tree_status_url
411
412 def GetViewVCUrl(self):
413 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000414 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 return self.viewvc_url
416
rmistry@google.com90752582014-01-14 21:04:50 +0000417 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000418 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000419
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000420 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000421 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000422
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000423 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000424 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000425
ukai@chromium.orge8077812012-02-03 03:41:46 +0000426 def GetIsGerrit(self):
427 """Return true if this repo is assosiated with gerrit code review system."""
428 if self.is_gerrit is None:
429 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
430 return self.is_gerrit
431
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000432 def GetGitEditor(self):
433 """Return the editor specified in the git config, or None if none is."""
434 if self.git_editor is None:
435 self.git_editor = self._GetConfig('core.editor', error_ok=True)
436 return self.git_editor or None
437
thestig@chromium.org44202a22014-03-11 19:22:18 +0000438 def GetLintRegex(self):
439 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
440 DEFAULT_LINT_REGEX)
441
442 def GetLintIgnoreRegex(self):
443 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
444 DEFAULT_LINT_IGNORE_REGEX)
445
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000446 def GetProject(self):
447 if not self.project:
448 self.project = self._GetRietveldConfig('project', error_ok=True)
449 return self.project
450
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000451 def _GetRietveldConfig(self, param, **kwargs):
452 return self._GetConfig('rietveld.' + param, **kwargs)
453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000454 def _GetConfig(self, param, **kwargs):
455 self.LazyUpdateIfNeeded()
456 return RunGit(['config', param], **kwargs).strip()
457
458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459def ShortBranchName(branch):
460 """Convert a name like 'refs/heads/foo' to just 'foo'."""
461 return branch.replace('refs/heads/', '')
462
463
464class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000465 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000467 global settings
468 if not settings:
469 # Happens when git_cl.py is used as a utility library.
470 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 settings.GetDefaultServerUrl()
472 self.branchref = branchref
473 if self.branchref:
474 self.branch = ShortBranchName(self.branchref)
475 else:
476 self.branch = None
477 self.rietveld_server = None
478 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000479 self.lookedup_issue = False
480 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 self.has_description = False
482 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000483 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000485 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000486 self.cc = None
487 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000488 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000489 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000490
491 def GetCCList(self):
492 """Return the users cc'd on this CL.
493
494 Return is a string suitable for passing to gcl with the --cc flag.
495 """
496 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000497 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000498 more_cc = ','.join(self.watchers)
499 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
500 return self.cc
501
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000502 def GetCCListWithoutDefault(self):
503 """Return the users cc'd on this CL excluding default ones."""
504 if self.cc is None:
505 self.cc = ','.join(self.watchers)
506 return self.cc
507
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000508 def SetWatchers(self, watchers):
509 """Set the list of email addresses that should be cc'd based on the changed
510 files in this CL.
511 """
512 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000513
514 def GetBranch(self):
515 """Returns the short branch name, e.g. 'master'."""
516 if not self.branch:
517 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
518 self.branch = ShortBranchName(self.branchref)
519 return self.branch
520
521 def GetBranchRef(self):
522 """Returns the full branch name, e.g. 'refs/heads/master'."""
523 self.GetBranch() # Poke the lazy loader.
524 return self.branchref
525
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000526 @staticmethod
527 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000528 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000529 e.g. 'origin', 'refs/heads/master'
530 """
531 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000532 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
533 error_ok=True).strip()
534 if upstream_branch:
535 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
536 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000537 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
538 error_ok=True).strip()
539 if upstream_branch:
540 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000541 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000542 # Fall back on trying a git-svn upstream branch.
543 if settings.GetIsGitSvn():
544 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000545 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000546 # Else, try to guess the origin remote.
547 remote_branches = RunGit(['branch', '-r']).split()
548 if 'origin/master' in remote_branches:
549 # Fall back on origin/master if it exits.
550 remote = 'origin'
551 upstream_branch = 'refs/heads/master'
552 elif 'origin/trunk' in remote_branches:
553 # Fall back on origin/trunk if it exists. Generally a shared
554 # git-svn clone
555 remote = 'origin'
556 upstream_branch = 'refs/heads/trunk'
557 else:
558 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000559Either pass complete "git diff"-style arguments, like
560 git cl upload origin/master
561or verify this branch is set up to track another (via the --track argument to
562"git checkout -b ...").""")
563
564 return remote, upstream_branch
565
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000566 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000567 return git_common.get_or_create_merge_base(self.GetBranch(),
568 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000569
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 def GetUpstreamBranch(self):
571 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000572 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573 if remote is not '.':
574 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
575 self.upstream_branch = upstream_branch
576 return self.upstream_branch
577
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000578 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000579 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000580 remote, branch = None, self.GetBranch()
581 seen_branches = set()
582 while branch not in seen_branches:
583 seen_branches.add(branch)
584 remote, branch = self.FetchUpstreamTuple(branch)
585 branch = ShortBranchName(branch)
586 if remote != '.' or branch.startswith('refs/remotes'):
587 break
588 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000589 remotes = RunGit(['remote'], error_ok=True).split()
590 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000591 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000592 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000593 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000594 logging.warning('Could not determine which remote this change is '
595 'associated with, so defaulting to "%s". This may '
596 'not be what you want. You may prevent this message '
597 'by running "git svn info" as documented here: %s',
598 self._remote,
599 GIT_INSTRUCTIONS_URL)
600 else:
601 logging.warn('Could not determine which remote this change is '
602 'associated with. You may prevent this message by '
603 'running "git svn info" as documented here: %s',
604 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000605 branch = 'HEAD'
606 if branch.startswith('refs/remotes'):
607 self._remote = (remote, branch)
608 else:
609 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000610 return self._remote
611
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000612 def GitSanityChecks(self, upstream_git_obj):
613 """Checks git repo status and ensures diff is from local commits."""
614
615 # Verify the commit we're diffing against is in our current branch.
616 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
617 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
618 if upstream_sha != common_ancestor:
619 print >> sys.stderr, (
620 'ERROR: %s is not in the current branch. You may need to rebase '
621 'your tracking branch' % upstream_sha)
622 return False
623
624 # List the commits inside the diff, and verify they are all local.
625 commits_in_diff = RunGit(
626 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
627 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
628 remote_branch = remote_branch.strip()
629 if code != 0:
630 _, remote_branch = self.GetRemoteBranch()
631
632 commits_in_remote = RunGit(
633 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
634
635 common_commits = set(commits_in_diff) & set(commits_in_remote)
636 if common_commits:
637 print >> sys.stderr, (
638 'ERROR: Your diff contains %d commits already in %s.\n'
639 'Run "git log --oneline %s..HEAD" to get a list of commits in '
640 'the diff. If you are using a custom git flow, you can override'
641 ' the reference used for this check with "git config '
642 'gitcl.remotebranch <git-ref>".' % (
643 len(common_commits), remote_branch, upstream_git_obj))
644 return False
645 return True
646
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000647 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000648 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000649
650 Returns None if it is not set.
651 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000652 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
653 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000654
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 def GetRemoteUrl(self):
656 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
657
658 Returns None if there is no remote.
659 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000660 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000661 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
662
663 # If URL is pointing to a local directory, it is probably a git cache.
664 if os.path.isdir(url):
665 url = RunGit(['config', 'remote.%s.url' % remote],
666 error_ok=True,
667 cwd=url).strip()
668 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669
670 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000671 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000674 self.issue = int(issue) or None if issue else None
675 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 return self.issue
677
678 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000679 if not self.rietveld_server:
680 # If we're on a branch then get the server potentially associated
681 # with that branch.
682 if self.GetIssue():
683 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
684 ['config', self._RietveldServer()], error_ok=True).strip())
685 if not self.rietveld_server:
686 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687 return self.rietveld_server
688
689 def GetIssueURL(self):
690 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000691 if not self.GetIssue():
692 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000693 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
694
695 def GetDescription(self, pretty=False):
696 if not self.has_description:
697 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000698 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000699 try:
700 self.description = self.RpcServer().get_description(issue).strip()
701 except urllib2.HTTPError, e:
702 if e.code == 404:
703 DieWithError(
704 ('\nWhile fetching the description for issue %d, received a '
705 '404 (not found)\n'
706 'error. It is likely that you deleted this '
707 'issue on the server. If this is the\n'
708 'case, please run\n\n'
709 ' git cl issue 0\n\n'
710 'to clear the association with the deleted issue. Then run '
711 'this command again.') % issue)
712 else:
713 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000714 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 self.has_description = True
716 if pretty:
717 wrapper = textwrap.TextWrapper()
718 wrapper.initial_indent = wrapper.subsequent_indent = ' '
719 return wrapper.fill(self.description)
720 return self.description
721
722 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000723 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000724 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 patchset = RunGit(['config', self._PatchsetSetting()],
726 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000727 self.patchset = int(patchset) or None if patchset else None
728 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 return self.patchset
730
731 def SetPatchset(self, patchset):
732 """Set this branch's patchset. If patchset=0, clears the patchset."""
733 if patchset:
734 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000735 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 else:
737 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000738 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000739 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000741 def GetMostRecentPatchset(self):
742 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000743
744 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000745 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000746 '/download/issue%s_%s.diff' % (issue, patchset))
747
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000748 def GetIssueProperties(self):
749 if self._props is None:
750 issue = self.GetIssue()
751 if not issue:
752 self._props = {}
753 else:
754 self._props = self.RpcServer().get_issue_properties(issue, True)
755 return self._props
756
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000757 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000758 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 def SetIssue(self, issue):
761 """Set this branch's issue. If issue=0, clears the issue."""
762 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000763 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 RunGit(['config', self._IssueSetting(), str(issue)])
765 if self.rietveld_server:
766 RunGit(['config', self._RietveldServer(), self.rietveld_server])
767 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000768 current_issue = self.GetIssue()
769 if current_issue:
770 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000771 self.issue = None
772 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000774 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000775 if not self.GitSanityChecks(upstream_branch):
776 DieWithError('\nGit sanity check failure')
777
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000778 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000779 if not root:
780 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000781 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000782
783 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000784 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000785 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000786 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000787 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000788 except subprocess2.CalledProcessError:
789 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000790 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000791 'This branch probably doesn\'t exist anymore. To reset the\n'
792 'tracking branch, please run\n'
793 ' git branch --set-upstream %s trunk\n'
794 'replacing trunk with origin/master or the relevant branch') %
795 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000796
maruel@chromium.org52424302012-08-29 15:14:30 +0000797 issue = self.GetIssue()
798 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000799 if issue:
800 description = self.GetDescription()
801 else:
802 # If the change was never uploaded, use the log messages of all commits
803 # up to the branch point, as git cl upload will prefill the description
804 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
806 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000807
808 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000809 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000810 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000811 name,
812 description,
813 absroot,
814 files,
815 issue,
816 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000817 author,
818 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000819
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000820 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000821 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000822
823 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000824 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000825 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000826 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000827 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000828 except presubmit_support.PresubmitFailure, e:
829 DieWithError(
830 ('%s\nMaybe your depot_tools is out of date?\n'
831 'If all fails, contact maruel@') % e)
832
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000833 def UpdateDescription(self, description):
834 self.description = description
835 return self.RpcServer().update_description(
836 self.GetIssue(), self.description)
837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000839 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000840 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000842 def SetFlag(self, flag, value):
843 """Patchset must match."""
844 if not self.GetPatchset():
845 DieWithError('The patchset needs to match. Send another patchset.')
846 try:
847 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000848 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000849 except urllib2.HTTPError, e:
850 if e.code == 404:
851 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
852 if e.code == 403:
853 DieWithError(
854 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
855 'match?') % (self.GetIssue(), self.GetPatchset()))
856 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000858 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 """Returns an upload.RpcServer() to access this review's rietveld instance.
860 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000861 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000862 self._rpc_server = rietveld.CachingRietveld(
863 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000864 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865
866 def _IssueSetting(self):
867 """Return the git setting that stores this change's issue."""
868 return 'branch.%s.rietveldissue' % self.GetBranch()
869
870 def _PatchsetSetting(self):
871 """Return the git setting that stores this change's most recent patchset."""
872 return 'branch.%s.rietveldpatchset' % self.GetBranch()
873
874 def _RietveldServer(self):
875 """Returns the git setting that stores this change's rietveld server."""
876 return 'branch.%s.rietveldserver' % self.GetBranch()
877
878
879def GetCodereviewSettingsInteractively():
880 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000881 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 server = settings.GetDefaultServerUrl(error_ok=True)
883 prompt = 'Rietveld server (host[:port])'
884 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000885 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886 if not server and not newserver:
887 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000888 if newserver:
889 newserver = gclient_utils.UpgradeToHttps(newserver)
890 if newserver != server:
891 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000893 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894 prompt = caption
895 if initial:
896 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000897 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 if new_val == 'x':
899 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000900 elif new_val:
901 if is_url:
902 new_val = gclient_utils.UpgradeToHttps(new_val)
903 if new_val != initial:
904 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000906 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000907 SetProperty(settings.GetDefaultPrivateFlag(),
908 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000910 'tree-status-url', False)
911 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000912 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913
914 # TODO: configure a default branch to diff against, rather than this
915 # svn-based hackery.
916
917
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000918class ChangeDescription(object):
919 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000920 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000921 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000922
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000923 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000924 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000925
agable@chromium.org42c20792013-09-12 17:34:49 +0000926 @property # www.logilab.org/ticket/89786
927 def description(self): # pylint: disable=E0202
928 return '\n'.join(self._description_lines)
929
930 def set_description(self, desc):
931 if isinstance(desc, basestring):
932 lines = desc.splitlines()
933 else:
934 lines = [line.rstrip() for line in desc]
935 while lines and not lines[0]:
936 lines.pop(0)
937 while lines and not lines[-1]:
938 lines.pop(-1)
939 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000940
941 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000943 assert isinstance(reviewers, list), reviewers
944 if not reviewers:
945 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000946 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000947
agable@chromium.org42c20792013-09-12 17:34:49 +0000948 # Get the set of R= and TBR= lines and remove them from the desciption.
949 regexp = re.compile(self.R_LINE)
950 matches = [regexp.match(line) for line in self._description_lines]
951 new_desc = [l for i, l in enumerate(self._description_lines)
952 if not matches[i]]
953 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000954
agable@chromium.org42c20792013-09-12 17:34:49 +0000955 # Construct new unified R= and TBR= lines.
956 r_names = []
957 tbr_names = []
958 for match in matches:
959 if not match:
960 continue
961 people = cleanup_list([match.group(2).strip()])
962 if match.group(1) == 'TBR':
963 tbr_names.extend(people)
964 else:
965 r_names.extend(people)
966 for name in r_names:
967 if name not in reviewers:
968 reviewers.append(name)
969 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
970 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
971
972 # Put the new lines in the description where the old first R= line was.
973 line_loc = next((i for i, match in enumerate(matches) if match), -1)
974 if 0 <= line_loc < len(self._description_lines):
975 if new_tbr_line:
976 self._description_lines.insert(line_loc, new_tbr_line)
977 if new_r_line:
978 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000979 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000980 if new_r_line:
981 self.append_footer(new_r_line)
982 if new_tbr_line:
983 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000984
985 def prompt(self):
986 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000987 self.set_description([
988 '# Enter a description of the change.',
989 '# This will be displayed on the codereview site.',
990 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000991 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000992 '--------------------',
993 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000994
agable@chromium.org42c20792013-09-12 17:34:49 +0000995 regexp = re.compile(self.BUG_LINE)
996 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000997 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000998 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000999 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001000 if not content:
1001 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001002 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001003
1004 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001005 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1006 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001007 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001008 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001009
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001010 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001011 if self._description_lines:
1012 # Add an empty line if either the last line or the new line isn't a tag.
1013 last_line = self._description_lines[-1]
1014 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1015 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1016 self._description_lines.append('')
1017 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001018
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001019 def get_reviewers(self):
1020 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001021 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1022 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001023 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001024
1025
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001026def get_approving_reviewers(props):
1027 """Retrieves the reviewers that approved a CL from the issue properties with
1028 messages.
1029
1030 Note that the list may contain reviewers that are not committer, thus are not
1031 considered by the CQ.
1032 """
1033 return sorted(
1034 set(
1035 message['sender']
1036 for message in props['messages']
1037 if message['approval'] and message['sender'] in props['reviewers']
1038 )
1039 )
1040
1041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042def FindCodereviewSettingsFile(filename='codereview.settings'):
1043 """Finds the given file starting in the cwd and going up.
1044
1045 Only looks up to the top of the repository unless an
1046 'inherit-review-settings-ok' file exists in the root of the repository.
1047 """
1048 inherit_ok_file = 'inherit-review-settings-ok'
1049 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001050 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1052 root = '/'
1053 while True:
1054 if filename in os.listdir(cwd):
1055 if os.path.isfile(os.path.join(cwd, filename)):
1056 return open(os.path.join(cwd, filename))
1057 if cwd == root:
1058 break
1059 cwd = os.path.dirname(cwd)
1060
1061
1062def LoadCodereviewSettingsFromFile(fileobj):
1063 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001064 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066 def SetProperty(name, setting, unset_error_ok=False):
1067 fullname = 'rietveld.' + name
1068 if setting in keyvals:
1069 RunGit(['config', fullname, keyvals[setting]])
1070 else:
1071 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1072
1073 SetProperty('server', 'CODE_REVIEW_SERVER')
1074 # Only server setting is required. Other settings can be absent.
1075 # In that case, we ignore errors raised during option deletion attempt.
1076 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001077 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1079 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001080 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001081 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1082 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001083 SetProperty('project', 'PROJECT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001085 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001086 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1089 #should be of the form
1090 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1091 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1092 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1093 keyvals['ORIGIN_URL_CONFIG']])
1094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001096def urlretrieve(source, destination):
1097 """urllib is broken for SSL connections via a proxy therefore we
1098 can't use urllib.urlretrieve()."""
1099 with open(destination, 'w') as f:
1100 f.write(urllib2.urlopen(source).read())
1101
1102
ukai@chromium.org712d6102013-11-27 00:52:58 +00001103def hasSheBang(fname):
1104 """Checks fname is a #! script."""
1105 with open(fname) as f:
1106 return f.read(2).startswith('#!')
1107
1108
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001109def DownloadHooks(force):
1110 """downloads hooks
1111
1112 Args:
1113 force: True to update hooks. False to install hooks if not present.
1114 """
1115 if not settings.GetIsGerrit():
1116 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001117 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001118 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1119 if not os.access(dst, os.X_OK):
1120 if os.path.exists(dst):
1121 if not force:
1122 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001123 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001124 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001125 if not hasSheBang(dst):
1126 DieWithError('Not a script: %s\n'
1127 'You need to download from\n%s\n'
1128 'into .git/hooks/commit-msg and '
1129 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001130 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1131 except Exception:
1132 if os.path.exists(dst):
1133 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001134 DieWithError('\nFailed to download hooks.\n'
1135 'You need to download from\n%s\n'
1136 'into .git/hooks/commit-msg and '
1137 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001138
1139
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001140@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001142 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001144 parser.add_option('--activate-update', action='store_true',
1145 help='activate auto-updating [rietveld] section in '
1146 '.git/config')
1147 parser.add_option('--deactivate-update', action='store_true',
1148 help='deactivate auto-updating [rietveld] section in '
1149 '.git/config')
1150 options, args = parser.parse_args(args)
1151
1152 if options.deactivate_update:
1153 RunGit(['config', 'rietveld.autoupdate', 'false'])
1154 return
1155
1156 if options.activate_update:
1157 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1158 return
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 if len(args) == 0:
1161 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001162 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 return 0
1164
1165 url = args[0]
1166 if not url.endswith('codereview.settings'):
1167 url = os.path.join(url, 'codereview.settings')
1168
1169 # Load code review settings and download hooks (if available).
1170 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001171 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 return 0
1173
1174
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001175def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001176 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001177 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1178 branch = ShortBranchName(branchref)
1179 _, args = parser.parse_args(args)
1180 if not args:
1181 print("Current base-url:")
1182 return RunGit(['config', 'branch.%s.base-url' % branch],
1183 error_ok=False).strip()
1184 else:
1185 print("Setting base-url to %s" % args[0])
1186 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1187 error_ok=False).strip()
1188
1189
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001191 """Show status of changelists.
1192
1193 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001194 - Red not sent for review or broken
1195 - Blue waiting for review
1196 - Yellow waiting for you to reply to review
1197 - Green LGTM'ed
1198 - Magenta in the commit queue
1199 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001200
1201 Also see 'git cl comments'.
1202 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 parser.add_option('--field',
1204 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 parser.add_option('-f', '--fast', action='store_true',
1206 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001208 if args:
1209 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001212 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 if options.field.startswith('desc'):
1214 print cl.GetDescription()
1215 elif options.field == 'id':
1216 issueid = cl.GetIssue()
1217 if issueid:
1218 print issueid
1219 elif options.field == 'patch':
1220 patchset = cl.GetPatchset()
1221 if patchset:
1222 print patchset
1223 elif options.field == 'url':
1224 url = cl.GetIssueURL()
1225 if url:
1226 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001227 return 0
1228
1229 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1230 if not branches:
1231 print('No local branch found.')
1232 return 0
1233
1234 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001235 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001236 alignment = max(5, max(len(b) for b in branches))
1237 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001238 # Adhoc thread pool to request data concurrently.
1239 output = Queue.Queue()
1240
1241 # Silence upload.py otherwise it becomes unweldly.
1242 upload.verbosity = 0
1243
1244 if not options.fast:
1245 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001246 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001247 c = Changelist(branchref=b)
1248 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001249 props = {}
1250 r = None
1251 if i:
1252 try:
1253 props = c.GetIssueProperties()
1254 r = c.GetApprovingReviewers() if i else None
1255 except urllib2.HTTPError:
1256 # The issue probably doesn't exist anymore.
1257 i += ' (broken)'
1258
1259 msgs = props.get('messages') or []
1260
1261 if not i:
1262 color = Fore.WHITE
1263 elif props.get('closed'):
1264 # Issue is closed.
1265 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001266 elif props.get('commit'):
1267 # Issue is in the commit queue.
1268 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001269 elif r:
1270 # Was LGTM'ed.
1271 color = Fore.GREEN
1272 elif not msgs:
1273 # No message was sent.
1274 color = Fore.RED
1275 elif msgs[-1]['sender'] != props.get('owner_email'):
1276 color = Fore.YELLOW
1277 else:
1278 color = Fore.BLUE
1279 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001280
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001281 # Process one branch synchronously to work through authentication, then
1282 # spawn threads to process all the other branches in parallel.
1283 if branches:
1284 fetch(branches[0])
1285 threads = [
1286 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001287 for t in threads:
1288 t.daemon = True
1289 t.start()
1290 else:
1291 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1292 for b in branches:
1293 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001294 url = c.GetIssueURL()
1295 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001296
1297 tmp = {}
1298 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001299 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001300 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001301 b, i, color = output.get()
1302 tmp[b] = (i, color)
1303 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001304 reset = Fore.RESET
1305 if not sys.stdout.isatty():
1306 color = ''
1307 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001308 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001309 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001310
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001311 cl = Changelist()
1312 print
1313 print 'Current branch:',
1314 if not cl.GetIssue():
1315 print 'no issue assigned.'
1316 return 0
1317 print cl.GetBranch()
1318 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1319 print 'Issue description:'
1320 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 return 0
1322
1323
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001324def colorize_CMDstatus_doc():
1325 """To be called once in main() to add colors to git cl status help."""
1326 colors = [i for i in dir(Fore) if i[0].isupper()]
1327
1328 def colorize_line(line):
1329 for color in colors:
1330 if color in line.upper():
1331 # Extract whitespaces first and the leading '-'.
1332 indent = len(line) - len(line.lstrip(' ')) + 1
1333 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1334 return line
1335
1336 lines = CMDstatus.__doc__.splitlines()
1337 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1338
1339
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001340@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001342 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343
1344 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001345 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001346 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347
1348 cl = Changelist()
1349 if len(args) > 0:
1350 try:
1351 issue = int(args[0])
1352 except ValueError:
1353 DieWithError('Pass a number to set the issue or none to list it.\n'
1354 'Maybe you want to run git cl status?')
1355 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001356 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 return 0
1358
1359
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001360def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001361 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001362 (_, args) = parser.parse_args(args)
1363 if args:
1364 parser.error('Unsupported argument: %s' % args)
1365
1366 cl = Changelist()
1367 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001368 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001369 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001370 if message['disapproval']:
1371 color = Fore.RED
1372 elif message['approval']:
1373 color = Fore.GREEN
1374 elif message['sender'] == data['owner_email']:
1375 color = Fore.MAGENTA
1376 else:
1377 color = Fore.BLUE
1378 print '\n%s%s %s%s' % (
1379 color, message['date'].split('.', 1)[0], message['sender'],
1380 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001381 if message['text'].strip():
1382 print '\n'.join(' ' + l for l in message['text'].splitlines())
1383 return 0
1384
1385
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001386def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001387 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001388 cl = Changelist()
1389 if not cl.GetIssue():
1390 DieWithError('This branch has no associated changelist.')
1391 description = ChangeDescription(cl.GetDescription())
1392 description.prompt()
1393 cl.UpdateDescription(description.description)
1394 return 0
1395
1396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397def CreateDescriptionFromLog(args):
1398 """Pulls out the commit log to use as a base for the CL description."""
1399 log_args = []
1400 if len(args) == 1 and not args[0].endswith('.'):
1401 log_args = [args[0] + '..']
1402 elif len(args) == 1 and args[0].endswith('...'):
1403 log_args = [args[0][:-1]]
1404 elif len(args) == 2:
1405 log_args = [args[0] + '..' + args[1]]
1406 else:
1407 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001408 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
1410
thestig@chromium.org44202a22014-03-11 19:22:18 +00001411def CMDlint(parser, args):
1412 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001413 parser.add_option('--filter', action='append', metavar='-x,+y',
1414 help='Comma-separated list of cpplint\'s category-filters')
1415 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001416
1417 # Access to a protected member _XX of a client class
1418 # pylint: disable=W0212
1419 try:
1420 import cpplint
1421 import cpplint_chromium
1422 except ImportError:
1423 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1424 return 1
1425
1426 # Change the current working directory before calling lint so that it
1427 # shows the correct base.
1428 previous_cwd = os.getcwd()
1429 os.chdir(settings.GetRoot())
1430 try:
1431 cl = Changelist()
1432 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1433 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001434 if not files:
1435 print "Cannot lint an empty CL"
1436 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001437
1438 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001439 command = args + files
1440 if options.filter:
1441 command = ['--filter=' + ','.join(options.filter)] + command
1442 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001443
1444 white_regex = re.compile(settings.GetLintRegex())
1445 black_regex = re.compile(settings.GetLintIgnoreRegex())
1446 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1447 for filename in filenames:
1448 if white_regex.match(filename):
1449 if black_regex.match(filename):
1450 print "Ignoring file %s" % filename
1451 else:
1452 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1453 extra_check_functions)
1454 else:
1455 print "Skipping file %s" % filename
1456 finally:
1457 os.chdir(previous_cwd)
1458 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1459 if cpplint._cpplint_state.error_count != 0:
1460 return 1
1461 return 0
1462
1463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001465 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001466 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001468 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001469 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 (options, args) = parser.parse_args(args)
1471
ukai@chromium.org259e4682012-10-25 07:36:33 +00001472 if not options.force and is_dirty_git_tree('presubmit'):
1473 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 return 1
1475
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001476 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 if args:
1478 base_branch = args[0]
1479 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001480 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001481 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001483 cl.RunHook(
1484 committing=not options.upload,
1485 may_prompt=False,
1486 verbose=options.verbose,
1487 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001488 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489
1490
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001491def AddChangeIdToCommitMessage(options, args):
1492 """Re-commits using the current message, assumes the commit hook is in
1493 place.
1494 """
1495 log_desc = options.message or CreateDescriptionFromLog(args)
1496 git_command = ['commit', '--amend', '-m', log_desc]
1497 RunGit(git_command)
1498 new_log_desc = CreateDescriptionFromLog(args)
1499 if CHANGE_ID in new_log_desc:
1500 print 'git-cl: Added Change-Id to commit message.'
1501 else:
1502 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1503
1504
ukai@chromium.orge8077812012-02-03 03:41:46 +00001505def GerritUpload(options, args, cl):
1506 """upload the current branch to gerrit."""
1507 # We assume the remote called "origin" is the one we want.
1508 # It is probably not worthwhile to support different workflows.
1509 remote = 'origin'
1510 branch = 'master'
1511 if options.target_branch:
1512 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001514 change_desc = ChangeDescription(
1515 options.message or CreateDescriptionFromLog(args))
1516 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001517 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001519 if CHANGE_ID not in change_desc.description:
1520 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001521
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001522 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001523 if len(commits) > 1:
1524 print('WARNING: This will upload %d commits. Run the following command '
1525 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001526 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001527 print('You can also use `git squash-branch` to squash these into a single'
1528 'commit.')
1529 ask_for_data('About to upload; enter to confirm.')
1530
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001531 if options.reviewers:
1532 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533
ukai@chromium.orge8077812012-02-03 03:41:46 +00001534 receive_options = []
1535 cc = cl.GetCCList().split(',')
1536 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001537 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001538 cc = filter(None, cc)
1539 if cc:
1540 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001541 if change_desc.get_reviewers():
1542 receive_options.extend(
1543 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544
ukai@chromium.orge8077812012-02-03 03:41:46 +00001545 git_command = ['push']
1546 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001547 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001548 ' '.join(receive_options))
1549 git_command += [remote, 'HEAD:refs/for/' + branch]
1550 RunGit(git_command)
1551 # TODO(ukai): parse Change-Id: and set issue number?
1552 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001553
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001555def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001556 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1558 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559 if options.emulate_svn_auto_props:
1560 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001561
1562 change_desc = None
1563
pgervais@chromium.org91141372014-01-09 23:27:20 +00001564 if options.email is not None:
1565 upload_args.extend(['--email', options.email])
1566
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001568 if options.title:
1569 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001570 if options.message:
1571 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001572 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001573 print ("This branch is associated with issue %s. "
1574 "Adding patch to that issue." % cl.GetIssue())
1575 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001576 if options.title:
1577 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001578 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001579 change_desc = ChangeDescription(message)
1580 if options.reviewers:
1581 change_desc.update_reviewers(options.reviewers)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001582 if options.auto_bots:
1583 masters = presubmit_support.DoGetTryMasters(
1584 change,
1585 change.LocalPaths(),
1586 settings.GetRoot(),
1587 None,
1588 None,
1589 options.verbose,
1590 sys.stdout)
1591
1592 if masters:
1593 change_description = change_desc.description + '\nCQ_TRYBOTS='
1594 lst = []
1595 for master, mapping in masters.iteritems():
1596 lst.append(master + ':' + ','.join(mapping.keys()))
1597 change_desc.set_description(change_description + ';'.join(lst))
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001598 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001599 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001600
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001601 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001602 print "Description is empty; aborting."
1603 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001604
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001605 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001606 if change_desc.get_reviewers():
1607 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001608 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001609 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001610 DieWithError("Must specify reviewers to send email.")
1611 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001612
1613 # We check this before applying rietveld.private assuming that in
1614 # rietveld.cc only addresses which we can send private CLs to are listed
1615 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1616 # --private is specified explicitly on the command line.
1617 if options.private:
1618 logging.warn('rietveld.cc is ignored since private flag is specified. '
1619 'You need to review and add them manually if necessary.')
1620 cc = cl.GetCCListWithoutDefault()
1621 else:
1622 cc = cl.GetCCList()
1623 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001624 if cc:
1625 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001627 if options.private or settings.GetDefaultPrivateFlag() == "True":
1628 upload_args.append('--private')
1629
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001630 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001631 if not options.find_copies:
1632 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001634 # Include the upstream repo's URL in the change -- this is useful for
1635 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001636 remote_url = cl.GetGitBaseUrlFromConfig()
1637 if not remote_url:
1638 if settings.GetIsGitSvn():
1639 # URL is dependent on the current directory.
1640 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1641 if data:
1642 keys = dict(line.split(': ', 1) for line in data.splitlines()
1643 if ': ' in line)
1644 remote_url = keys.get('URL', None)
1645 else:
1646 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1647 remote_url = (cl.GetRemoteUrl() + '@'
1648 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 if remote_url:
1650 upload_args.extend(['--base_url', remote_url])
1651
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001652 project = settings.GetProject()
1653 if project:
1654 upload_args.extend(['--project', project])
1655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001657 upload_args = ['upload'] + upload_args + args
1658 logging.info('upload.RealMain(%s)', upload_args)
1659 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001660 issue = int(issue)
1661 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001662 except KeyboardInterrupt:
1663 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001664 except:
1665 # If we got an exception after the user typed a description for their
1666 # change, back up the description before re-raising.
1667 if change_desc:
1668 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1669 print '\nGot exception while uploading -- saving description to %s\n' \
1670 % backup_path
1671 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001672 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673 backup_file.close()
1674 raise
1675
1676 if not cl.GetIssue():
1677 cl.SetIssue(issue)
1678 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001679
1680 if options.use_commit_queue:
1681 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 return 0
1683
1684
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001685def cleanup_list(l):
1686 """Fixes a list so that comma separated items are put as individual items.
1687
1688 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1689 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1690 """
1691 items = sum((i.split(',') for i in l), [])
1692 stripped_items = (i.strip() for i in items)
1693 return sorted(filter(None, stripped_items))
1694
1695
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001696@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001697def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001698 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001699 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1700 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001701 parser.add_option('--bypass-watchlists', action='store_true',
1702 dest='bypass_watchlists',
1703 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001704 parser.add_option('-f', action='store_true', dest='force',
1705 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001706 parser.add_option('-m', dest='message', help='message for patchset')
1707 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001708 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001709 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001710 help='reviewer email addresses')
1711 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001712 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001713 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001714 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001715 help='send email to reviewer immediately')
1716 parser.add_option("--emulate_svn_auto_props", action="store_true",
1717 dest="emulate_svn_auto_props",
1718 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001719 parser.add_option('-c', '--use-commit-queue', action='store_true',
1720 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001721 parser.add_option('--private', action='store_true',
1722 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001723 parser.add_option('--target_branch',
1724 help='When uploading to gerrit, remote branch to '
1725 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001726 parser.add_option('--email', default=None,
1727 help='email address to use to connect to Rietveld')
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001728 parser.add_option('--auto-bots', default=False, action='store_true',
1729 help='Autogenerate which trybots to use for this CL')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001730
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001731 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001732 (options, args) = parser.parse_args(args)
1733
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001734 if options.target_branch and not settings.GetIsGerrit():
1735 parser.error('Use --target_branch for non gerrit repository.')
1736
ukai@chromium.org259e4682012-10-25 07:36:33 +00001737 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001738 return 1
1739
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001740 options.reviewers = cleanup_list(options.reviewers)
1741 options.cc = cleanup_list(options.cc)
1742
ukai@chromium.orge8077812012-02-03 03:41:46 +00001743 cl = Changelist()
1744 if args:
1745 # TODO(ukai): is it ok for gerrit case?
1746 base_branch = args[0]
1747 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001748 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001749 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001750 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001751
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001752 # Apply watchlists on upload.
1753 change = cl.GetChange(base_branch, None)
1754 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1755 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001756 if not options.bypass_watchlists:
1757 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001758
ukai@chromium.orge8077812012-02-03 03:41:46 +00001759 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001760 if options.reviewers:
1761 # Set the reviewer list now so that presubmit checks can access it.
1762 change_description = ChangeDescription(change.FullDescriptionText())
1763 change_description.update_reviewers(options.reviewers)
1764 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001765 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001766 may_prompt=not options.force,
1767 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001768 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001769 if not hook_results.should_continue():
1770 return 1
1771 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001772 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001773
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001774 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001775 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001776 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001777 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001778 print ('The last upload made from this repository was patchset #%d but '
1779 'the most recent patchset on the server is #%d.'
1780 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001781 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1782 'from another machine or branch the patch you\'re uploading now '
1783 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001784 ask_for_data('About to upload; enter to confirm.')
1785
iannucci@chromium.org79540052012-10-19 23:15:26 +00001786 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001787 if settings.GetIsGerrit():
1788 return GerritUpload(options, args, cl)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001789 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001790 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001791 git_set_branch_value('last-upload-hash',
1792 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001793
1794 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001795
1796
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001797def IsSubmoduleMergeCommit(ref):
1798 # When submodules are added to the repo, we expect there to be a single
1799 # non-git-svn merge commit at remote HEAD with a signature comment.
1800 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001801 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001802 return RunGit(cmd) != ''
1803
1804
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001806 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001807
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001808 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001809 Updates changelog with metadata (e.g. pointer to review).
1810 Pushes/dcommits the code upstream.
1811 Updates review and closes.
1812 """
1813 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1814 help='bypass upload presubmit hook')
1815 parser.add_option('-m', dest='message',
1816 help="override review description")
1817 parser.add_option('-f', action='store_true', dest='force',
1818 help="force yes to questions (don't prompt)")
1819 parser.add_option('-c', dest='contributor',
1820 help="external contributor for patch (appended to " +
1821 "description and used as author for git). Should be " +
1822 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001823 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824 (options, args) = parser.parse_args(args)
1825 cl = Changelist()
1826
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001827 current = cl.GetBranch()
1828 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1829 if not settings.GetIsGitSvn() and remote == '.':
1830 print
1831 print 'Attempting to push branch %r into another local branch!' % current
1832 print
1833 print 'Either reparent this branch on top of origin/master:'
1834 print ' git reparent-branch --root'
1835 print
1836 print 'OR run `git rebase-update` if you think the parent branch is already'
1837 print 'committed.'
1838 print
1839 print ' Current parent: %r' % upstream_branch
1840 return 1
1841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842 if not args or cmd == 'push':
1843 # Default to merging against our best guess of the upstream branch.
1844 args = [cl.GetUpstreamBranch()]
1845
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001846 if options.contributor:
1847 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1848 print "Please provide contibutor as 'First Last <email@example.com>'"
1849 return 1
1850
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001852 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853
ukai@chromium.org259e4682012-10-25 07:36:33 +00001854 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855 return 1
1856
1857 # This rev-list syntax means "show all commits not in my branch that
1858 # are in base_branch".
1859 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1860 base_branch]).splitlines()
1861 if upstream_commits:
1862 print ('Base branch "%s" has %d commits '
1863 'not in this branch.' % (base_branch, len(upstream_commits)))
1864 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1865 return 1
1866
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001867 # This is the revision `svn dcommit` will commit on top of.
1868 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1869 '--pretty=format:%H'])
1870
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001872 # If the base_head is a submodule merge commit, the first parent of the
1873 # base_head should be a git-svn commit, which is what we're interested in.
1874 base_svn_head = base_branch
1875 if base_has_submodules:
1876 base_svn_head += '^1'
1877
1878 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001879 if extra_commits:
1880 print ('This branch has %d additional commits not upstreamed yet.'
1881 % len(extra_commits.splitlines()))
1882 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1883 'before attempting to %s.' % (base_branch, cmd))
1884 return 1
1885
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001886 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001887 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001888 author = None
1889 if options.contributor:
1890 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001891 hook_results = cl.RunHook(
1892 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001893 may_prompt=not options.force,
1894 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001895 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001896 if not hook_results.should_continue():
1897 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898
1899 if cmd == 'dcommit':
1900 # Check the tree status if the tree status URL is set.
1901 status = GetTreeStatus()
1902 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001903 print('The tree is closed. Please wait for it to reopen. Use '
1904 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 return 1
1906 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001907 print('Unable to determine tree status. Please verify manually and '
1908 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001909 else:
1910 breakpad.SendStack(
1911 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001912 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1913 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001914 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001916 change_desc = ChangeDescription(options.message)
1917 if not change_desc.description and cl.GetIssue():
1918 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001920 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001921 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001922 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001923 else:
1924 print 'No description set.'
1925 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1926 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001928 # Keep a separate copy for the commit message, because the commit message
1929 # contains the link to the Rietveld issue, while the Rietveld message contains
1930 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001931 # Keep a separate copy for the commit message.
1932 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001933 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001934
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001935 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001936 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001937 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001939 commit_desc.append_footer('Patch from %s.' % options.contributor)
1940
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001941 print('Description:')
1942 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943
1944 branches = [base_branch, cl.GetBranchRef()]
1945 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001946 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001947 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001949 # We want to squash all this branch's commits into one commit with the proper
1950 # description. We do this by doing a "reset --soft" to the base branch (which
1951 # keeps the working copy the same), then dcommitting that. If origin/master
1952 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1953 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001954 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001955 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1956 # Delete the branches if they exist.
1957 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1958 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1959 result = RunGitWithCode(showref_cmd)
1960 if result[0] == 0:
1961 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962
1963 # We might be in a directory that's present in this branch but not in the
1964 # trunk. Move up to the top of the tree so that git commands that expect a
1965 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001966 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001967 if rel_base_path:
1968 os.chdir(rel_base_path)
1969
1970 # Stuff our change into the merge branch.
1971 # We wrap in a try...finally block so if anything goes wrong,
1972 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001973 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001975 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1976 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001978 RunGit(
1979 [
1980 'commit', '--author', options.contributor,
1981 '-m', commit_desc.description,
1982 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001984 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001985 if base_has_submodules:
1986 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1987 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1988 RunGit(['checkout', CHERRY_PICK_BRANCH])
1989 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990 if cmd == 'push':
1991 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001992 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993 retcode, output = RunGitWithCode(
1994 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1995 logging.debug(output)
1996 else:
1997 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001998 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001999 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002000 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001 finally:
2002 # And then swap back to the original branch and clean up.
2003 RunGit(['checkout', '-q', cl.GetBranch()])
2004 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002005 if base_has_submodules:
2006 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007
2008 if cl.GetIssue():
2009 if cmd == 'dcommit' and 'Committed r' in output:
2010 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2011 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00002012 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
2013 for l in output.splitlines(False))
2014 match = filter(None, match)
2015 if len(match) != 1:
2016 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
2017 output)
2018 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019 else:
2020 return 1
2021 viewvc_url = settings.GetViewVCUrl()
2022 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002023 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00002024 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002025 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 print ('Closing issue '
2027 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002028 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002030 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002031 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00002032 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002033 if options.bypass_hooks:
2034 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2035 else:
2036 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002037 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002038 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002039
2040 if retcode == 0:
2041 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2042 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002043 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002044
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045 return 0
2046
2047
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002048@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002050 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002051 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002052 message = """This doesn't appear to be an SVN repository.
2053If your project has a git mirror with an upstream SVN master, you probably need
2054to run 'git svn init', see your project's git mirror documentation.
2055If your project has a true writeable upstream repository, you probably want
2056to run 'git cl push' instead.
2057Choose wisely, if you get this wrong, your commit might appear to succeed but
2058will instead be silently ignored."""
2059 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002060 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002061 return SendUpstream(parser, args, 'dcommit')
2062
2063
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002064@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002065def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002066 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002067 if settings.GetIsGitSvn():
2068 print('This appears to be an SVN repository.')
2069 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002070 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071 return SendUpstream(parser, args, 'push')
2072
2073
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002074@subcommand.usage('[upstream branch to apply against]')
2075def CMDpush(parser, args):
2076 """Temporary alias for 'land'.
2077 """
2078 print(
2079 "\n=======\n"
2080 "'git cl push' has been renamed to 'git cl land'.\n"
2081 "Currently they are treated as synonyms, but 'git cl push' will stop\n"
2082 "working after 2014/07/01.\n"
2083 "=======\n")
2084 now = datetime.datetime.utcfromtimestamp(time.time())
2085 if now > datetime.datetime(2014, 7, 1):
2086 print('******\nReally, you should not use this command anymore... \n'
2087 'Pausing 10 sec to help you remember :-)')
2088 for n in xrange(10):
2089 time.sleep(1)
2090 print('%s seconds...' % (n+1))
2091 print('******')
2092 return CMDland(parser, args)
2093
2094
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002095@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002097 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098 parser.add_option('-b', dest='newbranch',
2099 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002100 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002102 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2103 help='Change to the directory DIR immediately, '
2104 'before doing anything else.')
2105 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002106 help='failed patches spew .rej files rather than '
2107 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2109 help="don't commit after patch applies")
2110 (options, args) = parser.parse_args(args)
2111 if len(args) != 1:
2112 parser.print_help()
2113 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002114 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002116 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002117 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002118
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002119 if options.newbranch:
2120 if options.force:
2121 RunGit(['branch', '-D', options.newbranch],
2122 stderr=subprocess2.PIPE, error_ok=True)
2123 RunGit(['checkout', '-b', options.newbranch,
2124 Changelist().GetUpstreamBranch()])
2125
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002126 return PatchIssue(issue_arg, options.reject, options.nocommit,
2127 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002128
2129
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002130def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002131 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002132 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002133 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002134 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002135 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002136 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002137 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002138 # Assume it's a URL to the patch. Default to https.
2139 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002140 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002141 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002142 DieWithError('Must pass an issue ID or full URL for '
2143 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002144 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002145 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002146 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 # Switch up to the top-level directory, if necessary, in preparation for
2149 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002150 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151 if top:
2152 os.chdir(top)
2153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154 # Git patches have a/ at the beginning of source paths. We strip that out
2155 # with a sed script rather than the -p flag to patch so we can feed either
2156 # Git or svn-style patches into the same apply command.
2157 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002158 try:
2159 patch_data = subprocess2.check_output(
2160 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2161 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162 DieWithError('Git patch mungling failed.')
2163 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002164
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165 # We use "git apply" to apply the patch instead of "patch" so that we can
2166 # pick up file adds.
2167 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002168 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002169 if directory:
2170 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002171 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002172 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002173 elif IsGitVersionAtLeast('1.7.12'):
2174 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002175 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002176 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002177 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002178 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002179 DieWithError('Failed to apply the patch')
2180
2181 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002182 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2184 cl = Changelist()
2185 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002186 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002187 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002188 else:
2189 print "Patch applied to index."
2190 return 0
2191
2192
2193def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002194 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195 # Provide a wrapper for git svn rebase to help avoid accidental
2196 # git svn dcommit.
2197 # It's the only command that doesn't use parser at all since we just defer
2198 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002199
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002200 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201
2202
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002203def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002204 """Fetches the tree status and returns either 'open', 'closed',
2205 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002206 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002207 if url:
2208 status = urllib2.urlopen(url).read().lower()
2209 if status.find('closed') != -1 or status == '0':
2210 return 'closed'
2211 elif status.find('open') != -1 or status == '1':
2212 return 'open'
2213 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002214 return 'unset'
2215
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002217def GetTreeStatusReason():
2218 """Fetches the tree status from a json url and returns the message
2219 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002220 url = settings.GetTreeStatusUrl()
2221 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002222 connection = urllib2.urlopen(json_url)
2223 status = json.loads(connection.read())
2224 connection.close()
2225 return status['message']
2226
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002228def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002229 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002230 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002231 status = GetTreeStatus()
2232 if 'unset' == status:
2233 print 'You must configure your tree status URL by running "git cl config".'
2234 return 2
2235
2236 print "The tree is %s" % status
2237 print
2238 print GetTreeStatusReason()
2239 if status != 'open':
2240 return 1
2241 return 0
2242
2243
maruel@chromium.org15192402012-09-06 12:38:29 +00002244def CMDtry(parser, args):
2245 """Triggers a try job through Rietveld."""
2246 group = optparse.OptionGroup(parser, "Try job options")
2247 group.add_option(
2248 "-b", "--bot", action="append",
2249 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2250 "times to specify multiple builders. ex: "
2251 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2252 "the try server waterfall for the builders name and the tests "
2253 "available. Can also be used to specify gtest_filter, e.g. "
2254 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2255 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002256 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002257 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002258 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002259 "-r", "--revision",
2260 help="Revision to use for the try job; default: the "
2261 "revision will be determined by the try server; see "
2262 "its waterfall for more info")
2263 group.add_option(
2264 "-c", "--clobber", action="store_true", default=False,
2265 help="Force a clobber before building; e.g. don't do an "
2266 "incremental build")
2267 group.add_option(
2268 "--project",
2269 help="Override which project to use. Projects are defined "
2270 "server-side to define what default bot set to use")
2271 group.add_option(
2272 "-t", "--testfilter", action="append", default=[],
2273 help=("Apply a testfilter to all the selected builders. Unless the "
2274 "builders configurations are similar, use multiple "
2275 "--bot <builder>:<test> arguments."))
2276 group.add_option(
2277 "-n", "--name", help="Try job name; default to current branch name")
2278 parser.add_option_group(group)
2279 options, args = parser.parse_args(args)
2280
2281 if args:
2282 parser.error('Unknown arguments: %s' % args)
2283
2284 cl = Changelist()
2285 if not cl.GetIssue():
2286 parser.error('Need to upload first')
2287
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002288 props = cl.GetIssueProperties()
2289 if props.get('private'):
2290 parser.error('Cannot use trybots with private issue')
2291
maruel@chromium.org15192402012-09-06 12:38:29 +00002292 if not options.name:
2293 options.name = cl.GetBranch()
2294
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002295 if options.bot and not options.master:
2296 parser.error('For manually specified bots please also specify the '
2297 'tryserver master, e.g. "-m tryserver.chromium".')
2298
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002299 def GetMasterMap():
2300 # Process --bot and --testfilter.
2301 if not options.bot:
2302 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002303
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002304 # Get try masters from PRESUBMIT.py files.
2305 masters = presubmit_support.DoGetTryMasters(
2306 change,
2307 change.LocalPaths(),
2308 settings.GetRoot(),
2309 None,
2310 None,
2311 options.verbose,
2312 sys.stdout)
2313 if masters:
2314 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002315
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002316 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2317 options.bot = presubmit_support.DoGetTrySlaves(
2318 change,
2319 change.LocalPaths(),
2320 settings.GetRoot(),
2321 None,
2322 None,
2323 options.verbose,
2324 sys.stdout)
2325 if not options.bot:
2326 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002327
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002328 builders_and_tests = {}
2329 # TODO(machenbach): The old style command-line options don't support
2330 # multiple try masters yet.
2331 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2332 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2333
2334 for bot in old_style:
2335 if ':' in bot:
2336 builder, tests = bot.split(':', 1)
2337 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2338 elif ',' in bot:
2339 parser.error('Specify one bot per --bot flag')
2340 else:
2341 builders_and_tests.setdefault(bot, []).append('defaulttests')
2342
2343 for bot, tests in new_style:
2344 builders_and_tests.setdefault(bot, []).extend(tests)
2345
2346 # Return a master map with one master to be backwards compatible. The
2347 # master name defaults to an empty string, which will cause the master
2348 # not to be set on rietveld (deprecated).
2349 return {options.master: builders_and_tests}
2350
2351 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002352
maruel@chromium.org15192402012-09-06 12:38:29 +00002353 if options.testfilter:
2354 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002355 masters = dict((master, dict(
2356 (b, forced_tests) for b, t in slaves.iteritems()
2357 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002358
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002359 for builders in masters.itervalues():
2360 if any('triggered' in b for b in builders):
2361 print >> sys.stderr, (
2362 'ERROR You are trying to send a job to a triggered bot. This type of'
2363 ' bot requires an\ninitial job from a parent (usually a builder). '
2364 'Instead send your job to the parent.\n'
2365 'Bot list: %s' % builders)
2366 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002367
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002368 patchset = cl.GetMostRecentPatchset()
2369 if patchset and patchset != cl.GetPatchset():
2370 print(
2371 '\nWARNING Mismatch between local config and server. Did a previous '
2372 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2373 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002374 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002375 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002376 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002377 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002378 except urllib2.HTTPError, e:
2379 if e.code == 404:
2380 print('404 from rietveld; '
2381 'did you mean to use "git try" instead of "git cl try"?')
2382 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002383 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002384
2385 for (master, builders) in masters.iteritems():
2386 if master:
2387 print 'Master: %s' % master
2388 length = max(len(builder) for builder in builders)
2389 for builder in sorted(builders):
2390 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002391 return 0
2392
2393
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002394@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002395def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002396 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002397 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002398 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002399 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002401 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002402 if args:
2403 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002404 branch = cl.GetBranch()
2405 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002406 cl = Changelist()
2407 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002408
2409 # Clear configured merge-base, if there is one.
2410 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002411 else:
2412 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002413 return 0
2414
2415
thestig@chromium.org00858c82013-12-02 23:08:03 +00002416def CMDweb(parser, args):
2417 """Opens the current CL in the web browser."""
2418 _, args = parser.parse_args(args)
2419 if args:
2420 parser.error('Unrecognized args: %s' % ' '.join(args))
2421
2422 issue_url = Changelist().GetIssueURL()
2423 if not issue_url:
2424 print >> sys.stderr, 'ERROR No issue to open'
2425 return 1
2426
2427 webbrowser.open(issue_url)
2428 return 0
2429
2430
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002431def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002432 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002433 _, args = parser.parse_args(args)
2434 if args:
2435 parser.error('Unrecognized args: %s' % ' '.join(args))
2436 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002437 props = cl.GetIssueProperties()
2438 if props.get('private'):
2439 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002440 cl.SetFlag('commit', '1')
2441 return 0
2442
2443
groby@chromium.org411034a2013-02-26 15:12:01 +00002444def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002445 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002446 _, args = parser.parse_args(args)
2447 if args:
2448 parser.error('Unrecognized args: %s' % ' '.join(args))
2449 cl = Changelist()
2450 # Ensure there actually is an issue to close.
2451 cl.GetDescription()
2452 cl.CloseIssue()
2453 return 0
2454
2455
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002456def CMDdiff(parser, args):
2457 """shows differences between local tree and last upload."""
2458 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002459 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002460 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002461 if not issue:
2462 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002463 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002464 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002465
2466 # Create a new branch based on the merge-base
2467 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2468 try:
2469 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002470 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002471 if rtn != 0:
2472 return rtn
2473
2474 # Switch back to starting brand and diff against the temporary
2475 # branch containing the latest rietveld patch.
2476 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2477 finally:
2478 RunGit(['checkout', '-q', branch])
2479 RunGit(['branch', '-D', TMP_BRANCH])
2480
2481 return 0
2482
2483
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002484def CMDowners(parser, args):
2485 """interactively find the owners for reviewing"""
2486 parser.add_option(
2487 '--no-color',
2488 action='store_true',
2489 help='Use this option to disable color output')
2490 options, args = parser.parse_args(args)
2491
2492 author = RunGit(['config', 'user.email']).strip() or None
2493
2494 cl = Changelist()
2495
2496 if args:
2497 if len(args) > 1:
2498 parser.error('Unknown args')
2499 base_branch = args[0]
2500 else:
2501 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002502 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002503
2504 change = cl.GetChange(base_branch, None)
2505 return owners_finder.OwnersFinder(
2506 [f.LocalPath() for f in
2507 cl.GetChange(base_branch, None).AffectedFiles()],
2508 change.RepositoryRoot(), author,
2509 fopen=file, os_path=os.path, glob=glob.glob,
2510 disable_color=options.no_color).run()
2511
2512
enne@chromium.org555cfe42014-01-29 18:21:39 +00002513@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002514def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002515 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002516 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002517 parser.add_option('--full', action='store_true',
2518 help='Reformat the full content of all touched files')
2519 parser.add_option('--dry-run', action='store_true',
2520 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002521 parser.add_option('--diff', action='store_true',
2522 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002523 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002524
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002525 # git diff generates paths against the root of the repository. Change
2526 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002527 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002528 if rel_base_path:
2529 os.chdir(rel_base_path)
2530
digit@chromium.org29e47272013-05-17 17:01:46 +00002531 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002532 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002533 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002534 # Only list the names of modified files.
2535 diff_cmd.append('--name-only')
2536 else:
2537 # Only generate context-less patches.
2538 diff_cmd.append('-U0')
2539
2540 # Grab the merge-base commit, i.e. the upstream commit of the current
2541 # branch when it was created or the last time it was rebased. This is
2542 # to cover the case where the user may have called "git fetch origin",
2543 # moving the origin branch to a newer commit, but hasn't rebased yet.
2544 upstream_commit = None
2545 cl = Changelist()
2546 upstream_branch = cl.GetUpstreamBranch()
2547 if upstream_branch:
2548 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2549 upstream_commit = upstream_commit.strip()
2550
2551 if not upstream_commit:
2552 DieWithError('Could not find base commit for this branch. '
2553 'Are you in detached state?')
2554
2555 diff_cmd.append(upstream_commit)
2556
2557 # Handle source file filtering.
2558 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002559 if args:
2560 for arg in args:
2561 if os.path.isdir(arg):
2562 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2563 elif os.path.isfile(arg):
2564 diff_cmd.append(arg)
2565 else:
2566 DieWithError('Argument "%s" is not a file or a directory' % arg)
2567 else:
2568 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002569 diff_output = RunGit(diff_cmd)
2570
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002571 top_dir = os.path.normpath(
2572 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2573
2574 # Locate the clang-format binary in the checkout
2575 try:
2576 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2577 except clang_format.NotFoundError, e:
2578 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002579
digit@chromium.org29e47272013-05-17 17:01:46 +00002580 if opts.full:
2581 # diff_output is a list of files to send to clang-format.
2582 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002583 if not files:
2584 print "Nothing to format."
2585 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002586 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002587 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002588 cmd.append('-i')
2589 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002590 if opts.diff:
2591 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002592 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002593 env = os.environ.copy()
2594 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002595 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002596 try:
2597 script = clang_format.FindClangFormatScriptInChromiumTree(
2598 'clang-format-diff.py')
2599 except clang_format.NotFoundError, e:
2600 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002601
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002602 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002603 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002604 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002605
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002606 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002607 if opts.diff:
2608 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002609 if opts.dry_run and len(stdout) > 0:
2610 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002611
2612 return 0
2613
2614
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002615class OptionParser(optparse.OptionParser):
2616 """Creates the option parse and add --verbose support."""
2617 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002618 optparse.OptionParser.__init__(
2619 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002620 self.add_option(
2621 '-v', '--verbose', action='count', default=0,
2622 help='Use 2 times for more debugging info')
2623
2624 def parse_args(self, args=None, values=None):
2625 options, args = optparse.OptionParser.parse_args(self, args, values)
2626 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2627 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2628 return options, args
2629
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002630
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002631def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002632 if sys.hexversion < 0x02060000:
2633 print >> sys.stderr, (
2634 '\nYour python version %s is unsupported, please upgrade.\n' %
2635 sys.version.split(' ', 1)[0])
2636 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002637
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002638 # Reload settings.
2639 global settings
2640 settings = Settings()
2641
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002642 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002643 dispatcher = subcommand.CommandDispatcher(__name__)
2644 try:
2645 return dispatcher.execute(OptionParser(), argv)
2646 except urllib2.HTTPError, e:
2647 if e.code != 500:
2648 raise
2649 DieWithError(
2650 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2651 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002652
2653
2654if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002655 # These affect sys.stdout so do it outside of main() to simplify mocks in
2656 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002657 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002658 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002659 sys.exit(main(sys.argv[1:]))