blob: 738370953979831504c2ac1b10b100e0d3c1c2d6 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000038import git_common
39import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000041import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000043import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000044import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import watchlists
46
maruel@chromium.org0633fb42013-08-16 20:06:14 +000047__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000049DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000050POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000052GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000053CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054
thestig@chromium.org44202a22014-03-11 19:22:18 +000055# Valid extensions for files we want to lint.
56DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
57DEFAULT_LINT_IGNORE_REGEX = r"$^"
58
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000059# Shortcut since it quickly becomes redundant.
60Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000061
maruel@chromium.orgddd59412011-11-30 14:20:38 +000062# Initialized in main()
63settings = None
64
65
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000067 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 sys.exit(1)
69
70
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000071def GetNoGitPagerEnv():
72 env = os.environ.copy()
73 # 'cat' is a magical git string that disables pagers on all platforms.
74 env['GIT_PAGER'] = 'cat'
75 return env
76
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000079 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000080 except subprocess2.CalledProcessError as e:
81 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000082 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 'Command "%s" failed.\n%s' % (
85 ' '.join(args), error_message or e.stdout or ''))
86 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087
88
89def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000090 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000091 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000092
93
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000094def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000096 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000097 if suppress_stderr:
98 stderr = subprocess2.VOID
99 else:
100 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000101 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000102 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000103 stdout=subprocess2.PIPE,
104 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000105 return code, out[0]
106 except ValueError:
107 # When the subprocess fails, it returns None. That triggers a ValueError
108 # when trying to unpack the return value into (out, code).
109 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000112def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000113 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000114 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000115 return (version.startswith(prefix) and
116 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000117
118
maruel@chromium.org90541732011-04-01 17:54:18 +0000119def ask_for_data(prompt):
120 try:
121 return raw_input(prompt)
122 except KeyboardInterrupt:
123 # Hide the exception.
124 sys.exit(1)
125
126
iannucci@chromium.org79540052012-10-19 23:15:26 +0000127def git_set_branch_value(key, value):
128 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000129 if not branch:
130 return
131
132 cmd = ['config']
133 if isinstance(value, int):
134 cmd.append('--int')
135 git_key = 'branch.%s.%s' % (branch, key)
136 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000137
138
139def git_get_branch_default(key, default):
140 branch = Changelist().GetBranch()
141 if branch:
142 git_key = 'branch.%s.%s' % (branch, key)
143 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
144 try:
145 return int(stdout.strip())
146 except ValueError:
147 pass
148 return default
149
150
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000151def add_git_similarity(parser):
152 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000153 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000154 help='Sets the percentage that a pair of files need to match in order to'
155 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156 parser.add_option(
157 '--find-copies', action='store_true',
158 help='Allows git to look for copies.')
159 parser.add_option(
160 '--no-find-copies', action='store_false', dest='find_copies',
161 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000162
163 old_parser_args = parser.parse_args
164 def Parse(args):
165 options, args = old_parser_args(args)
166
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000167 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170 print('Note: Saving similarity of %d%% in git config.'
171 % options.similarity)
172 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000173
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 options.similarity = max(0, min(options.similarity, 100))
175
176 if options.find_copies is None:
177 options.find_copies = bool(
178 git_get_branch_default('git-find-copies', True))
179 else:
180 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000181
182 print('Using %d%% similarity for rename/copy detection. '
183 'Override with --similarity.' % options.similarity)
184
185 return options, args
186 parser.parse_args = Parse
187
188
ukai@chromium.org259e4682012-10-25 07:36:33 +0000189def is_dirty_git_tree(cmd):
190 # Make sure index is up-to-date before running diff-index.
191 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
192 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
193 if dirty:
194 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
195 print 'Uncommitted files: (git diff-index --name-status HEAD)'
196 print dirty[:4096]
197 if len(dirty) > 4096:
198 print '... (run "git diff-index --name-status HEAD" to see full output).'
199 return True
200 return False
201
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000202
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000203def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
204 """Return the corresponding git ref if |base_url| together with |glob_spec|
205 matches the full |url|.
206
207 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
208 """
209 fetch_suburl, as_ref = glob_spec.split(':')
210 if allow_wildcards:
211 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
212 if glob_match:
213 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
214 # "branches/{472,597,648}/src:refs/remotes/svn/*".
215 branch_re = re.escape(base_url)
216 if glob_match.group(1):
217 branch_re += '/' + re.escape(glob_match.group(1))
218 wildcard = glob_match.group(2)
219 if wildcard == '*':
220 branch_re += '([^/]*)'
221 else:
222 # Escape and replace surrounding braces with parentheses and commas
223 # with pipe symbols.
224 wildcard = re.escape(wildcard)
225 wildcard = re.sub('^\\\\{', '(', wildcard)
226 wildcard = re.sub('\\\\,', '|', wildcard)
227 wildcard = re.sub('\\\\}$', ')', wildcard)
228 branch_re += wildcard
229 if glob_match.group(3):
230 branch_re += re.escape(glob_match.group(3))
231 match = re.match(branch_re, url)
232 if match:
233 return re.sub('\*$', match.group(1), as_ref)
234
235 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
236 if fetch_suburl:
237 full_url = base_url + '/' + fetch_suburl
238 else:
239 full_url = base_url
240 if full_url == url:
241 return as_ref
242 return None
243
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000244
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000246 """Prints statistics about the change to the user."""
247 # --no-ext-diff is broken in some versions of Git, so try to work around
248 # this by overriding the environment (but there is still a problem if the
249 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000250 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000251 if 'GIT_EXTERNAL_DIFF' in env:
252 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000253
254 if find_copies:
255 similarity_options = ['--find-copies-harder', '-l100000',
256 '-C%s' % similarity]
257 else:
258 similarity_options = ['-M%s' % similarity]
259
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000260 try:
261 stdout = sys.stdout.fileno()
262 except AttributeError:
263 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000264 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000265 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000266 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000267 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000268
269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000270class Settings(object):
271 def __init__(self):
272 self.default_server = None
273 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000274 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 self.is_git_svn = None
276 self.svn_branch = None
277 self.tree_status_url = None
278 self.viewvc_url = None
279 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000280 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000281 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000282 self.project = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000283
284 def LazyUpdateIfNeeded(self):
285 """Updates the settings from a codereview.settings file, if available."""
286 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000287 # The only value that actually changes the behavior is
288 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000289 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000290 error_ok=True
291 ).strip().lower()
292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000293 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000294 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000296 # set updated to True to avoid infinite calling loop
297 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000298 self.updated = True
299 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000300 self.updated = True
301
302 def GetDefaultServerUrl(self, error_ok=False):
303 if not self.default_server:
304 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000305 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000306 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000307 if error_ok:
308 return self.default_server
309 if not self.default_server:
310 error_message = ('Could not find settings file. You must configure '
311 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000312 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000313 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000314 return self.default_server
315
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000316 @staticmethod
317 def GetRelativeRoot():
318 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000319
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000320 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000321 if self.root is None:
322 self.root = os.path.abspath(self.GetRelativeRoot())
323 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000324
325 def GetIsGitSvn(self):
326 """Return true if this repo looks like it's using git-svn."""
327 if self.is_git_svn is None:
bratell@opera.com05fb9112014-07-07 09:30:23 +0000328 # If you have any "svn-remote.*" config keys, we think you're using svn.
329 self.is_git_svn = RunGitWithCode(
330 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000331 return self.is_git_svn
332
333 def GetSVNBranch(self):
334 if self.svn_branch is None:
335 if not self.GetIsGitSvn():
336 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
337
338 # Try to figure out which remote branch we're based on.
339 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000340 # 1) iterate through our branch history and find the svn URL.
341 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000342
343 # regexp matching the git-svn line that contains the URL.
344 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
345
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000346 # We don't want to go through all of history, so read a line from the
347 # pipe at a time.
348 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000349 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000350 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
351 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000352 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000353 for line in proc.stdout:
354 match = git_svn_re.match(line)
355 if match:
356 url = match.group(1)
357 proc.stdout.close() # Cut pipe.
358 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000359
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000360 if url:
361 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
362 remotes = RunGit(['config', '--get-regexp',
363 r'^svn-remote\..*\.url']).splitlines()
364 for remote in remotes:
365 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000366 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000367 remote = match.group(1)
368 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000369 rewrite_root = RunGit(
370 ['config', 'svn-remote.%s.rewriteRoot' % remote],
371 error_ok=True).strip()
372 if rewrite_root:
373 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000374 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000375 ['config', 'svn-remote.%s.fetch' % remote],
376 error_ok=True).strip()
377 if fetch_spec:
378 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
379 if self.svn_branch:
380 break
381 branch_spec = RunGit(
382 ['config', 'svn-remote.%s.branches' % remote],
383 error_ok=True).strip()
384 if branch_spec:
385 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
386 if self.svn_branch:
387 break
388 tag_spec = RunGit(
389 ['config', 'svn-remote.%s.tags' % remote],
390 error_ok=True).strip()
391 if tag_spec:
392 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
393 if self.svn_branch:
394 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000395
396 if not self.svn_branch:
397 DieWithError('Can\'t guess svn branch -- try specifying it on the '
398 'command line')
399
400 return self.svn_branch
401
402 def GetTreeStatusUrl(self, error_ok=False):
403 if not self.tree_status_url:
404 error_message = ('You must configure your tree status URL by running '
405 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000406 self.tree_status_url = self._GetRietveldConfig(
407 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000408 return self.tree_status_url
409
410 def GetViewVCUrl(self):
411 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000412 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000413 return self.viewvc_url
414
rmistry@google.com90752582014-01-14 21:04:50 +0000415 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000416 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000417
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000418 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000419 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000420
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000421 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000422 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000423
ukai@chromium.orge8077812012-02-03 03:41:46 +0000424 def GetIsGerrit(self):
425 """Return true if this repo is assosiated with gerrit code review system."""
426 if self.is_gerrit is None:
427 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
428 return self.is_gerrit
429
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000430 def GetGitEditor(self):
431 """Return the editor specified in the git config, or None if none is."""
432 if self.git_editor is None:
433 self.git_editor = self._GetConfig('core.editor', error_ok=True)
434 return self.git_editor or None
435
thestig@chromium.org44202a22014-03-11 19:22:18 +0000436 def GetLintRegex(self):
437 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
438 DEFAULT_LINT_REGEX)
439
440 def GetLintIgnoreRegex(self):
441 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
442 DEFAULT_LINT_IGNORE_REGEX)
443
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000444 def GetProject(self):
445 if not self.project:
446 self.project = self._GetRietveldConfig('project', error_ok=True)
447 return self.project
448
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000449 def _GetRietveldConfig(self, param, **kwargs):
450 return self._GetConfig('rietveld.' + param, **kwargs)
451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 def _GetConfig(self, param, **kwargs):
453 self.LazyUpdateIfNeeded()
454 return RunGit(['config', param], **kwargs).strip()
455
456
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000457def ShortBranchName(branch):
458 """Convert a name like 'refs/heads/foo' to just 'foo'."""
459 return branch.replace('refs/heads/', '')
460
461
462class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000463 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000464 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000465 global settings
466 if not settings:
467 # Happens when git_cl.py is used as a utility library.
468 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469 settings.GetDefaultServerUrl()
470 self.branchref = branchref
471 if self.branchref:
472 self.branch = ShortBranchName(self.branchref)
473 else:
474 self.branch = None
475 self.rietveld_server = None
476 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000477 self.lookedup_issue = False
478 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000479 self.has_description = False
480 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000481 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000482 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000483 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000484 self.cc = None
485 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000486 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000487 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000488
489 def GetCCList(self):
490 """Return the users cc'd on this CL.
491
492 Return is a string suitable for passing to gcl with the --cc flag.
493 """
494 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000495 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000496 more_cc = ','.join(self.watchers)
497 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
498 return self.cc
499
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000500 def GetCCListWithoutDefault(self):
501 """Return the users cc'd on this CL excluding default ones."""
502 if self.cc is None:
503 self.cc = ','.join(self.watchers)
504 return self.cc
505
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000506 def SetWatchers(self, watchers):
507 """Set the list of email addresses that should be cc'd based on the changed
508 files in this CL.
509 """
510 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000511
512 def GetBranch(self):
513 """Returns the short branch name, e.g. 'master'."""
514 if not self.branch:
515 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
516 self.branch = ShortBranchName(self.branchref)
517 return self.branch
518
519 def GetBranchRef(self):
520 """Returns the full branch name, e.g. 'refs/heads/master'."""
521 self.GetBranch() # Poke the lazy loader.
522 return self.branchref
523
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000524 @staticmethod
525 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000526 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000527 e.g. 'origin', 'refs/heads/master'
528 """
529 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000530 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
531 error_ok=True).strip()
532 if upstream_branch:
533 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
534 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000535 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
536 error_ok=True).strip()
537 if upstream_branch:
538 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000540 # Fall back on trying a git-svn upstream branch.
541 if settings.GetIsGitSvn():
542 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000543 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000544 # Else, try to guess the origin remote.
545 remote_branches = RunGit(['branch', '-r']).split()
546 if 'origin/master' in remote_branches:
547 # Fall back on origin/master if it exits.
548 remote = 'origin'
549 upstream_branch = 'refs/heads/master'
550 elif 'origin/trunk' in remote_branches:
551 # Fall back on origin/trunk if it exists. Generally a shared
552 # git-svn clone
553 remote = 'origin'
554 upstream_branch = 'refs/heads/trunk'
555 else:
556 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000557Either pass complete "git diff"-style arguments, like
558 git cl upload origin/master
559or verify this branch is set up to track another (via the --track argument to
560"git checkout -b ...").""")
561
562 return remote, upstream_branch
563
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000564 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000565 return git_common.get_or_create_merge_base(self.GetBranch(),
566 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000567
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000568 def GetUpstreamBranch(self):
569 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000570 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 if remote is not '.':
572 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
573 self.upstream_branch = upstream_branch
574 return self.upstream_branch
575
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000576 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000577 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000578 remote, branch = None, self.GetBranch()
579 seen_branches = set()
580 while branch not in seen_branches:
581 seen_branches.add(branch)
582 remote, branch = self.FetchUpstreamTuple(branch)
583 branch = ShortBranchName(branch)
584 if remote != '.' or branch.startswith('refs/remotes'):
585 break
586 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000587 remotes = RunGit(['remote'], error_ok=True).split()
588 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000589 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000590 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000591 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000592 logging.warning('Could not determine which remote this change is '
593 'associated with, so defaulting to "%s". This may '
594 'not be what you want. You may prevent this message '
595 'by running "git svn info" as documented here: %s',
596 self._remote,
597 GIT_INSTRUCTIONS_URL)
598 else:
599 logging.warn('Could not determine which remote this change is '
600 'associated with. You may prevent this message by '
601 'running "git svn info" as documented here: %s',
602 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000603 branch = 'HEAD'
604 if branch.startswith('refs/remotes'):
605 self._remote = (remote, branch)
606 else:
607 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000608 return self._remote
609
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000610 def GitSanityChecks(self, upstream_git_obj):
611 """Checks git repo status and ensures diff is from local commits."""
612
613 # Verify the commit we're diffing against is in our current branch.
614 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
615 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
616 if upstream_sha != common_ancestor:
617 print >> sys.stderr, (
618 'ERROR: %s is not in the current branch. You may need to rebase '
619 'your tracking branch' % upstream_sha)
620 return False
621
622 # List the commits inside the diff, and verify they are all local.
623 commits_in_diff = RunGit(
624 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
625 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
626 remote_branch = remote_branch.strip()
627 if code != 0:
628 _, remote_branch = self.GetRemoteBranch()
629
630 commits_in_remote = RunGit(
631 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
632
633 common_commits = set(commits_in_diff) & set(commits_in_remote)
634 if common_commits:
635 print >> sys.stderr, (
636 'ERROR: Your diff contains %d commits already in %s.\n'
637 'Run "git log --oneline %s..HEAD" to get a list of commits in '
638 'the diff. If you are using a custom git flow, you can override'
639 ' the reference used for this check with "git config '
640 'gitcl.remotebranch <git-ref>".' % (
641 len(common_commits), remote_branch, upstream_git_obj))
642 return False
643 return True
644
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000645 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000646 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000647
648 Returns None if it is not set.
649 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000650 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
651 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000652
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653 def GetRemoteUrl(self):
654 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
655
656 Returns None if there is no remote.
657 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000658 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000659 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
660
661 # If URL is pointing to a local directory, it is probably a git cache.
662 if os.path.isdir(url):
663 url = RunGit(['config', 'remote.%s.url' % remote],
664 error_ok=True,
665 cwd=url).strip()
666 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667
668 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000669 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000670 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 self.issue = int(issue) or None if issue else None
673 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674 return self.issue
675
676 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000677 if not self.rietveld_server:
678 # If we're on a branch then get the server potentially associated
679 # with that branch.
680 if self.GetIssue():
681 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
682 ['config', self._RietveldServer()], error_ok=True).strip())
683 if not self.rietveld_server:
684 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685 return self.rietveld_server
686
687 def GetIssueURL(self):
688 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000689 if not self.GetIssue():
690 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
692
693 def GetDescription(self, pretty=False):
694 if not self.has_description:
695 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000696 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000697 try:
698 self.description = self.RpcServer().get_description(issue).strip()
699 except urllib2.HTTPError, e:
700 if e.code == 404:
701 DieWithError(
702 ('\nWhile fetching the description for issue %d, received a '
703 '404 (not found)\n'
704 'error. It is likely that you deleted this '
705 'issue on the server. If this is the\n'
706 'case, please run\n\n'
707 ' git cl issue 0\n\n'
708 'to clear the association with the deleted issue. Then run '
709 'this command again.') % issue)
710 else:
711 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000712 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713 self.has_description = True
714 if pretty:
715 wrapper = textwrap.TextWrapper()
716 wrapper.initial_indent = wrapper.subsequent_indent = ' '
717 return wrapper.fill(self.description)
718 return self.description
719
720 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000721 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000722 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723 patchset = RunGit(['config', self._PatchsetSetting()],
724 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000725 self.patchset = int(patchset) or None if patchset else None
726 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727 return self.patchset
728
729 def SetPatchset(self, patchset):
730 """Set this branch's patchset. If patchset=0, clears the patchset."""
731 if patchset:
732 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000733 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 else:
735 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000736 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000737 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000739 def GetMostRecentPatchset(self):
740 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000741
742 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000743 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000744 '/download/issue%s_%s.diff' % (issue, patchset))
745
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000746 def GetIssueProperties(self):
747 if self._props is None:
748 issue = self.GetIssue()
749 if not issue:
750 self._props = {}
751 else:
752 self._props = self.RpcServer().get_issue_properties(issue, True)
753 return self._props
754
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000755 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000756 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000757
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 def SetIssue(self, issue):
759 """Set this branch's issue. If issue=0, clears the issue."""
760 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000761 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 RunGit(['config', self._IssueSetting(), str(issue)])
763 if self.rietveld_server:
764 RunGit(['config', self._RietveldServer(), self.rietveld_server])
765 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000766 current_issue = self.GetIssue()
767 if current_issue:
768 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000769 self.issue = None
770 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000772 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000773 if not self.GitSanityChecks(upstream_branch):
774 DieWithError('\nGit sanity check failure')
775
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000776 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000777 if not root:
778 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000779 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000780
781 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000782 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000783 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000784 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000785 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000786 except subprocess2.CalledProcessError:
787 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000788 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000789 'This branch probably doesn\'t exist anymore. To reset the\n'
790 'tracking branch, please run\n'
791 ' git branch --set-upstream %s trunk\n'
792 'replacing trunk with origin/master or the relevant branch') %
793 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000794
maruel@chromium.org52424302012-08-29 15:14:30 +0000795 issue = self.GetIssue()
796 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000797 if issue:
798 description = self.GetDescription()
799 else:
800 # If the change was never uploaded, use the log messages of all commits
801 # up to the branch point, as git cl upload will prefill the description
802 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000803 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
804 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000805
806 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000807 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000808 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000809 name,
810 description,
811 absroot,
812 files,
813 issue,
814 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000815 author,
816 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000817
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000818 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000819 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000820
821 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000822 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000823 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000824 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000825 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000826 except presubmit_support.PresubmitFailure, e:
827 DieWithError(
828 ('%s\nMaybe your depot_tools is out of date?\n'
829 'If all fails, contact maruel@') % e)
830
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000831 def UpdateDescription(self, description):
832 self.description = description
833 return self.RpcServer().update_description(
834 self.GetIssue(), self.description)
835
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000837 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000838 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000840 def SetFlag(self, flag, value):
841 """Patchset must match."""
842 if not self.GetPatchset():
843 DieWithError('The patchset needs to match. Send another patchset.')
844 try:
845 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000846 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000847 except urllib2.HTTPError, e:
848 if e.code == 404:
849 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
850 if e.code == 403:
851 DieWithError(
852 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
853 'match?') % (self.GetIssue(), self.GetPatchset()))
854 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000856 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857 """Returns an upload.RpcServer() to access this review's rietveld instance.
858 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000859 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000860 self._rpc_server = rietveld.CachingRietveld(
861 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000862 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863
864 def _IssueSetting(self):
865 """Return the git setting that stores this change's issue."""
866 return 'branch.%s.rietveldissue' % self.GetBranch()
867
868 def _PatchsetSetting(self):
869 """Return the git setting that stores this change's most recent patchset."""
870 return 'branch.%s.rietveldpatchset' % self.GetBranch()
871
872 def _RietveldServer(self):
873 """Returns the git setting that stores this change's rietveld server."""
874 return 'branch.%s.rietveldserver' % self.GetBranch()
875
876
877def GetCodereviewSettingsInteractively():
878 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000879 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 server = settings.GetDefaultServerUrl(error_ok=True)
881 prompt = 'Rietveld server (host[:port])'
882 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000883 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 if not server and not newserver:
885 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000886 if newserver:
887 newserver = gclient_utils.UpgradeToHttps(newserver)
888 if newserver != server:
889 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000891 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 prompt = caption
893 if initial:
894 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000895 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 if new_val == 'x':
897 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000898 elif new_val:
899 if is_url:
900 new_val = gclient_utils.UpgradeToHttps(new_val)
901 if new_val != initial:
902 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000904 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000905 SetProperty(settings.GetDefaultPrivateFlag(),
906 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000908 'tree-status-url', False)
909 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000910 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911
912 # TODO: configure a default branch to diff against, rather than this
913 # svn-based hackery.
914
915
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000916class ChangeDescription(object):
917 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000918 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000919 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000920
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000921 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000922 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000923
agable@chromium.org42c20792013-09-12 17:34:49 +0000924 @property # www.logilab.org/ticket/89786
925 def description(self): # pylint: disable=E0202
926 return '\n'.join(self._description_lines)
927
928 def set_description(self, desc):
929 if isinstance(desc, basestring):
930 lines = desc.splitlines()
931 else:
932 lines = [line.rstrip() for line in desc]
933 while lines and not lines[0]:
934 lines.pop(0)
935 while lines and not lines[-1]:
936 lines.pop(-1)
937 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000938
939 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000940 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000941 assert isinstance(reviewers, list), reviewers
942 if not reviewers:
943 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000944 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000945
agable@chromium.org42c20792013-09-12 17:34:49 +0000946 # Get the set of R= and TBR= lines and remove them from the desciption.
947 regexp = re.compile(self.R_LINE)
948 matches = [regexp.match(line) for line in self._description_lines]
949 new_desc = [l for i, l in enumerate(self._description_lines)
950 if not matches[i]]
951 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000952
agable@chromium.org42c20792013-09-12 17:34:49 +0000953 # Construct new unified R= and TBR= lines.
954 r_names = []
955 tbr_names = []
956 for match in matches:
957 if not match:
958 continue
959 people = cleanup_list([match.group(2).strip()])
960 if match.group(1) == 'TBR':
961 tbr_names.extend(people)
962 else:
963 r_names.extend(people)
964 for name in r_names:
965 if name not in reviewers:
966 reviewers.append(name)
967 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
968 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
969
970 # Put the new lines in the description where the old first R= line was.
971 line_loc = next((i for i, match in enumerate(matches) if match), -1)
972 if 0 <= line_loc < len(self._description_lines):
973 if new_tbr_line:
974 self._description_lines.insert(line_loc, new_tbr_line)
975 if new_r_line:
976 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000977 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000978 if new_r_line:
979 self.append_footer(new_r_line)
980 if new_tbr_line:
981 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000982
983 def prompt(self):
984 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000985 self.set_description([
986 '# Enter a description of the change.',
987 '# This will be displayed on the codereview site.',
988 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000989 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000990 '--------------------',
991 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000992
agable@chromium.org42c20792013-09-12 17:34:49 +0000993 regexp = re.compile(self.BUG_LINE)
994 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000995 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000996 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000997 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000998 if not content:
999 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001000 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001001
1002 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001003 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1004 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001005 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001006 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001007
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001008 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001009 if self._description_lines:
1010 # Add an empty line if either the last line or the new line isn't a tag.
1011 last_line = self._description_lines[-1]
1012 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1013 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1014 self._description_lines.append('')
1015 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001016
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001017 def get_reviewers(self):
1018 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001019 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1020 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001021 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001022
1023
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001024def get_approving_reviewers(props):
1025 """Retrieves the reviewers that approved a CL from the issue properties with
1026 messages.
1027
1028 Note that the list may contain reviewers that are not committer, thus are not
1029 considered by the CQ.
1030 """
1031 return sorted(
1032 set(
1033 message['sender']
1034 for message in props['messages']
1035 if message['approval'] and message['sender'] in props['reviewers']
1036 )
1037 )
1038
1039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040def FindCodereviewSettingsFile(filename='codereview.settings'):
1041 """Finds the given file starting in the cwd and going up.
1042
1043 Only looks up to the top of the repository unless an
1044 'inherit-review-settings-ok' file exists in the root of the repository.
1045 """
1046 inherit_ok_file = 'inherit-review-settings-ok'
1047 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001048 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1050 root = '/'
1051 while True:
1052 if filename in os.listdir(cwd):
1053 if os.path.isfile(os.path.join(cwd, filename)):
1054 return open(os.path.join(cwd, filename))
1055 if cwd == root:
1056 break
1057 cwd = os.path.dirname(cwd)
1058
1059
1060def LoadCodereviewSettingsFromFile(fileobj):
1061 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001062 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 def SetProperty(name, setting, unset_error_ok=False):
1065 fullname = 'rietveld.' + name
1066 if setting in keyvals:
1067 RunGit(['config', fullname, keyvals[setting]])
1068 else:
1069 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1070
1071 SetProperty('server', 'CODE_REVIEW_SERVER')
1072 # Only server setting is required. Other settings can be absent.
1073 # In that case, we ignore errors raised during option deletion attempt.
1074 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001075 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1077 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001078 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001079 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1080 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001081 SetProperty('project', 'PROJECT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001083 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001084 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001085
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1087 #should be of the form
1088 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1089 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1090 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1091 keyvals['ORIGIN_URL_CONFIG']])
1092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001094def urlretrieve(source, destination):
1095 """urllib is broken for SSL connections via a proxy therefore we
1096 can't use urllib.urlretrieve()."""
1097 with open(destination, 'w') as f:
1098 f.write(urllib2.urlopen(source).read())
1099
1100
ukai@chromium.org712d6102013-11-27 00:52:58 +00001101def hasSheBang(fname):
1102 """Checks fname is a #! script."""
1103 with open(fname) as f:
1104 return f.read(2).startswith('#!')
1105
1106
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001107def DownloadHooks(force):
1108 """downloads hooks
1109
1110 Args:
1111 force: True to update hooks. False to install hooks if not present.
1112 """
1113 if not settings.GetIsGerrit():
1114 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001115 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001116 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1117 if not os.access(dst, os.X_OK):
1118 if os.path.exists(dst):
1119 if not force:
1120 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001121 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001122 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001123 if not hasSheBang(dst):
1124 DieWithError('Not a script: %s\n'
1125 'You need to download from\n%s\n'
1126 'into .git/hooks/commit-msg and '
1127 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001128 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1129 except Exception:
1130 if os.path.exists(dst):
1131 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001132 DieWithError('\nFailed to download hooks.\n'
1133 'You need to download from\n%s\n'
1134 'into .git/hooks/commit-msg and '
1135 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001136
1137
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001138@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001140 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001142 parser.add_option('--activate-update', action='store_true',
1143 help='activate auto-updating [rietveld] section in '
1144 '.git/config')
1145 parser.add_option('--deactivate-update', action='store_true',
1146 help='deactivate auto-updating [rietveld] section in '
1147 '.git/config')
1148 options, args = parser.parse_args(args)
1149
1150 if options.deactivate_update:
1151 RunGit(['config', 'rietveld.autoupdate', 'false'])
1152 return
1153
1154 if options.activate_update:
1155 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1156 return
1157
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 if len(args) == 0:
1159 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001160 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161 return 0
1162
1163 url = args[0]
1164 if not url.endswith('codereview.settings'):
1165 url = os.path.join(url, 'codereview.settings')
1166
1167 # Load code review settings and download hooks (if available).
1168 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001169 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 return 0
1171
1172
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001173def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001174 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001175 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1176 branch = ShortBranchName(branchref)
1177 _, args = parser.parse_args(args)
1178 if not args:
1179 print("Current base-url:")
1180 return RunGit(['config', 'branch.%s.base-url' % branch],
1181 error_ok=False).strip()
1182 else:
1183 print("Setting base-url to %s" % args[0])
1184 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1185 error_ok=False).strip()
1186
1187
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001189 """Show status of changelists.
1190
1191 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001192 - Red not sent for review or broken
1193 - Blue waiting for review
1194 - Yellow waiting for you to reply to review
1195 - Green LGTM'ed
1196 - Magenta in the commit queue
1197 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001198
1199 Also see 'git cl comments'.
1200 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 parser.add_option('--field',
1202 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001203 parser.add_option('-f', '--fast', action='store_true',
1204 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001206 if args:
1207 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001210 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if options.field.startswith('desc'):
1212 print cl.GetDescription()
1213 elif options.field == 'id':
1214 issueid = cl.GetIssue()
1215 if issueid:
1216 print issueid
1217 elif options.field == 'patch':
1218 patchset = cl.GetPatchset()
1219 if patchset:
1220 print patchset
1221 elif options.field == 'url':
1222 url = cl.GetIssueURL()
1223 if url:
1224 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001225 return 0
1226
1227 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1228 if not branches:
1229 print('No local branch found.')
1230 return 0
1231
1232 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001233 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001234 alignment = max(5, max(len(b) for b in branches))
1235 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001236 # Adhoc thread pool to request data concurrently.
1237 output = Queue.Queue()
1238
1239 # Silence upload.py otherwise it becomes unweldly.
1240 upload.verbosity = 0
1241
1242 if not options.fast:
1243 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001244 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001245 c = Changelist(branchref=b)
1246 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001247 props = {}
1248 r = None
1249 if i:
1250 try:
1251 props = c.GetIssueProperties()
1252 r = c.GetApprovingReviewers() if i else None
1253 except urllib2.HTTPError:
1254 # The issue probably doesn't exist anymore.
1255 i += ' (broken)'
1256
1257 msgs = props.get('messages') or []
1258
1259 if not i:
1260 color = Fore.WHITE
1261 elif props.get('closed'):
1262 # Issue is closed.
1263 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001264 elif props.get('commit'):
1265 # Issue is in the commit queue.
1266 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001267 elif r:
1268 # Was LGTM'ed.
1269 color = Fore.GREEN
1270 elif not msgs:
1271 # No message was sent.
1272 color = Fore.RED
1273 elif msgs[-1]['sender'] != props.get('owner_email'):
1274 color = Fore.YELLOW
1275 else:
1276 color = Fore.BLUE
1277 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001278
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001279 # Process one branch synchronously to work through authentication, then
1280 # spawn threads to process all the other branches in parallel.
1281 if branches:
1282 fetch(branches[0])
1283 threads = [
1284 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001285 for t in threads:
1286 t.daemon = True
1287 t.start()
1288 else:
1289 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1290 for b in branches:
1291 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001292 url = c.GetIssueURL()
1293 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001294
1295 tmp = {}
1296 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001297 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001298 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001299 b, i, color = output.get()
1300 tmp[b] = (i, color)
1301 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001302 reset = Fore.RESET
1303 if not sys.stdout.isatty():
1304 color = ''
1305 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001306 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001307 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001308
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001309 cl = Changelist()
1310 print
1311 print 'Current branch:',
1312 if not cl.GetIssue():
1313 print 'no issue assigned.'
1314 return 0
1315 print cl.GetBranch()
1316 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1317 print 'Issue description:'
1318 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319 return 0
1320
1321
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001322def colorize_CMDstatus_doc():
1323 """To be called once in main() to add colors to git cl status help."""
1324 colors = [i for i in dir(Fore) if i[0].isupper()]
1325
1326 def colorize_line(line):
1327 for color in colors:
1328 if color in line.upper():
1329 # Extract whitespaces first and the leading '-'.
1330 indent = len(line) - len(line.lstrip(' ')) + 1
1331 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1332 return line
1333
1334 lines = CMDstatus.__doc__.splitlines()
1335 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1336
1337
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001338@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001340 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341
1342 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001343 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001344 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001345
1346 cl = Changelist()
1347 if len(args) > 0:
1348 try:
1349 issue = int(args[0])
1350 except ValueError:
1351 DieWithError('Pass a number to set the issue or none to list it.\n'
1352 'Maybe you want to run git cl status?')
1353 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001354 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 return 0
1356
1357
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001358def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001359 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001360 (_, args) = parser.parse_args(args)
1361 if args:
1362 parser.error('Unsupported argument: %s' % args)
1363
1364 cl = Changelist()
1365 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001366 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001367 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001368 if message['disapproval']:
1369 color = Fore.RED
1370 elif message['approval']:
1371 color = Fore.GREEN
1372 elif message['sender'] == data['owner_email']:
1373 color = Fore.MAGENTA
1374 else:
1375 color = Fore.BLUE
1376 print '\n%s%s %s%s' % (
1377 color, message['date'].split('.', 1)[0], message['sender'],
1378 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001379 if message['text'].strip():
1380 print '\n'.join(' ' + l for l in message['text'].splitlines())
1381 return 0
1382
1383
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001384def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001385 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001386 cl = Changelist()
1387 if not cl.GetIssue():
1388 DieWithError('This branch has no associated changelist.')
1389 description = ChangeDescription(cl.GetDescription())
1390 description.prompt()
1391 cl.UpdateDescription(description.description)
1392 return 0
1393
1394
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395def CreateDescriptionFromLog(args):
1396 """Pulls out the commit log to use as a base for the CL description."""
1397 log_args = []
1398 if len(args) == 1 and not args[0].endswith('.'):
1399 log_args = [args[0] + '..']
1400 elif len(args) == 1 and args[0].endswith('...'):
1401 log_args = [args[0][:-1]]
1402 elif len(args) == 2:
1403 log_args = [args[0] + '..' + args[1]]
1404 else:
1405 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001406 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
1408
thestig@chromium.org44202a22014-03-11 19:22:18 +00001409def CMDlint(parser, args):
1410 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001411 parser.add_option('--filter', action='append', metavar='-x,+y',
1412 help='Comma-separated list of cpplint\'s category-filters')
1413 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001414
1415 # Access to a protected member _XX of a client class
1416 # pylint: disable=W0212
1417 try:
1418 import cpplint
1419 import cpplint_chromium
1420 except ImportError:
1421 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1422 return 1
1423
1424 # Change the current working directory before calling lint so that it
1425 # shows the correct base.
1426 previous_cwd = os.getcwd()
1427 os.chdir(settings.GetRoot())
1428 try:
1429 cl = Changelist()
1430 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1431 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001432 if not files:
1433 print "Cannot lint an empty CL"
1434 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001435
1436 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001437 command = args + files
1438 if options.filter:
1439 command = ['--filter=' + ','.join(options.filter)] + command
1440 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001441
1442 white_regex = re.compile(settings.GetLintRegex())
1443 black_regex = re.compile(settings.GetLintIgnoreRegex())
1444 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1445 for filename in filenames:
1446 if white_regex.match(filename):
1447 if black_regex.match(filename):
1448 print "Ignoring file %s" % filename
1449 else:
1450 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1451 extra_check_functions)
1452 else:
1453 print "Skipping file %s" % filename
1454 finally:
1455 os.chdir(previous_cwd)
1456 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1457 if cpplint._cpplint_state.error_count != 0:
1458 return 1
1459 return 0
1460
1461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001463 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001464 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001466 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001467 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 (options, args) = parser.parse_args(args)
1469
ukai@chromium.org259e4682012-10-25 07:36:33 +00001470 if not options.force and is_dirty_git_tree('presubmit'):
1471 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 return 1
1473
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001474 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 if args:
1476 base_branch = args[0]
1477 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001478 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001479 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001481 cl.RunHook(
1482 committing=not options.upload,
1483 may_prompt=False,
1484 verbose=options.verbose,
1485 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001486 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487
1488
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001489def AddChangeIdToCommitMessage(options, args):
1490 """Re-commits using the current message, assumes the commit hook is in
1491 place.
1492 """
1493 log_desc = options.message or CreateDescriptionFromLog(args)
1494 git_command = ['commit', '--amend', '-m', log_desc]
1495 RunGit(git_command)
1496 new_log_desc = CreateDescriptionFromLog(args)
1497 if CHANGE_ID in new_log_desc:
1498 print 'git-cl: Added Change-Id to commit message.'
1499 else:
1500 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1501
1502
ukai@chromium.orge8077812012-02-03 03:41:46 +00001503def GerritUpload(options, args, cl):
1504 """upload the current branch to gerrit."""
1505 # We assume the remote called "origin" is the one we want.
1506 # It is probably not worthwhile to support different workflows.
1507 remote = 'origin'
1508 branch = 'master'
1509 if options.target_branch:
1510 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001511
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001512 change_desc = ChangeDescription(
1513 options.message or CreateDescriptionFromLog(args))
1514 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001515 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001517 if CHANGE_ID not in change_desc.description:
1518 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001519
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001520 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001521 if len(commits) > 1:
1522 print('WARNING: This will upload %d commits. Run the following command '
1523 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001524 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001525 print('You can also use `git squash-branch` to squash these into a single'
1526 'commit.')
1527 ask_for_data('About to upload; enter to confirm.')
1528
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001529 if options.reviewers:
1530 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531
ukai@chromium.orge8077812012-02-03 03:41:46 +00001532 receive_options = []
1533 cc = cl.GetCCList().split(',')
1534 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001535 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001536 cc = filter(None, cc)
1537 if cc:
1538 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001539 if change_desc.get_reviewers():
1540 receive_options.extend(
1541 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542
ukai@chromium.orge8077812012-02-03 03:41:46 +00001543 git_command = ['push']
1544 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001545 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001546 ' '.join(receive_options))
1547 git_command += [remote, 'HEAD:refs/for/' + branch]
1548 RunGit(git_command)
1549 # TODO(ukai): parse Change-Id: and set issue number?
1550 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001551
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001553def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001554 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001555 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1556 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557 if options.emulate_svn_auto_props:
1558 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559
1560 change_desc = None
1561
pgervais@chromium.org91141372014-01-09 23:27:20 +00001562 if options.email is not None:
1563 upload_args.extend(['--email', options.email])
1564
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001566 if options.title:
1567 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001568 if options.message:
1569 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001570 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571 print ("This branch is associated with issue %s. "
1572 "Adding patch to that issue." % cl.GetIssue())
1573 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001574 if options.title:
1575 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001576 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001577 change_desc = ChangeDescription(message)
1578 if options.reviewers:
1579 change_desc.update_reviewers(options.reviewers)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001580 if options.auto_bots:
1581 masters = presubmit_support.DoGetTryMasters(
1582 change,
1583 change.LocalPaths(),
1584 settings.GetRoot(),
1585 None,
1586 None,
1587 options.verbose,
1588 sys.stdout)
1589
1590 if masters:
1591 change_description = change_desc.description + '\nCQ_TRYBOTS='
1592 lst = []
1593 for master, mapping in masters.iteritems():
1594 lst.append(master + ':' + ','.join(mapping.keys()))
1595 change_desc.set_description(change_description + ';'.join(lst))
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001596 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001597 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001598
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001599 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600 print "Description is empty; aborting."
1601 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001602
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001603 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001604 if change_desc.get_reviewers():
1605 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001606 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001607 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001608 DieWithError("Must specify reviewers to send email.")
1609 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001610
1611 # We check this before applying rietveld.private assuming that in
1612 # rietveld.cc only addresses which we can send private CLs to are listed
1613 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1614 # --private is specified explicitly on the command line.
1615 if options.private:
1616 logging.warn('rietveld.cc is ignored since private flag is specified. '
1617 'You need to review and add them manually if necessary.')
1618 cc = cl.GetCCListWithoutDefault()
1619 else:
1620 cc = cl.GetCCList()
1621 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001622 if cc:
1623 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001625 if options.private or settings.GetDefaultPrivateFlag() == "True":
1626 upload_args.append('--private')
1627
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001628 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001629 if not options.find_copies:
1630 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632 # Include the upstream repo's URL in the change -- this is useful for
1633 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001634 remote_url = cl.GetGitBaseUrlFromConfig()
1635 if not remote_url:
1636 if settings.GetIsGitSvn():
1637 # URL is dependent on the current directory.
1638 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1639 if data:
1640 keys = dict(line.split(': ', 1) for line in data.splitlines()
1641 if ': ' in line)
1642 remote_url = keys.get('URL', None)
1643 else:
1644 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1645 remote_url = (cl.GetRemoteUrl() + '@'
1646 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647 if remote_url:
1648 upload_args.extend(['--base_url', remote_url])
1649
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001650 project = settings.GetProject()
1651 if project:
1652 upload_args.extend(['--project', project])
1653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001655 upload_args = ['upload'] + upload_args + args
1656 logging.info('upload.RealMain(%s)', upload_args)
1657 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001658 issue = int(issue)
1659 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001660 except KeyboardInterrupt:
1661 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662 except:
1663 # If we got an exception after the user typed a description for their
1664 # change, back up the description before re-raising.
1665 if change_desc:
1666 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1667 print '\nGot exception while uploading -- saving description to %s\n' \
1668 % backup_path
1669 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001670 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001671 backup_file.close()
1672 raise
1673
1674 if not cl.GetIssue():
1675 cl.SetIssue(issue)
1676 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001677
1678 if options.use_commit_queue:
1679 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680 return 0
1681
1682
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001683def cleanup_list(l):
1684 """Fixes a list so that comma separated items are put as individual items.
1685
1686 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1687 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1688 """
1689 items = sum((i.split(',') for i in l), [])
1690 stripped_items = (i.strip() for i in items)
1691 return sorted(filter(None, stripped_items))
1692
1693
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001694@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001695def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001696 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001697 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1698 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001699 parser.add_option('--bypass-watchlists', action='store_true',
1700 dest='bypass_watchlists',
1701 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001702 parser.add_option('-f', action='store_true', dest='force',
1703 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001704 parser.add_option('-m', dest='message', help='message for patchset')
1705 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001706 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001707 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001708 help='reviewer email addresses')
1709 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001710 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001711 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001712 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001713 help='send email to reviewer immediately')
1714 parser.add_option("--emulate_svn_auto_props", action="store_true",
1715 dest="emulate_svn_auto_props",
1716 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001717 parser.add_option('-c', '--use-commit-queue', action='store_true',
1718 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001719 parser.add_option('--private', action='store_true',
1720 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001721 parser.add_option('--target_branch',
1722 help='When uploading to gerrit, remote branch to '
1723 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001724 parser.add_option('--email', default=None,
1725 help='email address to use to connect to Rietveld')
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001726 parser.add_option('--auto-bots', default=False, action='store_true',
1727 help='Autogenerate which trybots to use for this CL')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001728
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001729 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001730 (options, args) = parser.parse_args(args)
1731
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001732 if options.target_branch and not settings.GetIsGerrit():
1733 parser.error('Use --target_branch for non gerrit repository.')
1734
ukai@chromium.org259e4682012-10-25 07:36:33 +00001735 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001736 return 1
1737
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001738 options.reviewers = cleanup_list(options.reviewers)
1739 options.cc = cleanup_list(options.cc)
1740
ukai@chromium.orge8077812012-02-03 03:41:46 +00001741 cl = Changelist()
1742 if args:
1743 # TODO(ukai): is it ok for gerrit case?
1744 base_branch = args[0]
1745 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001746 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001747 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001748 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001749
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001750 # Apply watchlists on upload.
1751 change = cl.GetChange(base_branch, None)
1752 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1753 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001754 if not options.bypass_watchlists:
1755 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001756
ukai@chromium.orge8077812012-02-03 03:41:46 +00001757 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001758 if options.reviewers:
1759 # Set the reviewer list now so that presubmit checks can access it.
1760 change_description = ChangeDescription(change.FullDescriptionText())
1761 change_description.update_reviewers(options.reviewers)
1762 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001763 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001764 may_prompt=not options.force,
1765 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001766 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001767 if not hook_results.should_continue():
1768 return 1
1769 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001770 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001771
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001772 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001773 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001774 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001775 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001776 print ('The last upload made from this repository was patchset #%d but '
1777 'the most recent patchset on the server is #%d.'
1778 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001779 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1780 'from another machine or branch the patch you\'re uploading now '
1781 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001782 ask_for_data('About to upload; enter to confirm.')
1783
iannucci@chromium.org79540052012-10-19 23:15:26 +00001784 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001785 if settings.GetIsGerrit():
1786 return GerritUpload(options, args, cl)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001787 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001788 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001789 git_set_branch_value('last-upload-hash',
1790 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001791
1792 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001793
1794
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001795def IsSubmoduleMergeCommit(ref):
1796 # When submodules are added to the repo, we expect there to be a single
1797 # non-git-svn merge commit at remote HEAD with a signature comment.
1798 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001799 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001800 return RunGit(cmd) != ''
1801
1802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001804 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001806 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001807 Updates changelog with metadata (e.g. pointer to review).
1808 Pushes/dcommits the code upstream.
1809 Updates review and closes.
1810 """
1811 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1812 help='bypass upload presubmit hook')
1813 parser.add_option('-m', dest='message',
1814 help="override review description")
1815 parser.add_option('-f', action='store_true', dest='force',
1816 help="force yes to questions (don't prompt)")
1817 parser.add_option('-c', dest='contributor',
1818 help="external contributor for patch (appended to " +
1819 "description and used as author for git). Should be " +
1820 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001821 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001822 (options, args) = parser.parse_args(args)
1823 cl = Changelist()
1824
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001825 current = cl.GetBranch()
1826 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1827 if not settings.GetIsGitSvn() and remote == '.':
1828 print
1829 print 'Attempting to push branch %r into another local branch!' % current
1830 print
1831 print 'Either reparent this branch on top of origin/master:'
1832 print ' git reparent-branch --root'
1833 print
1834 print 'OR run `git rebase-update` if you think the parent branch is already'
1835 print 'committed.'
1836 print
1837 print ' Current parent: %r' % upstream_branch
1838 return 1
1839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 if not args or cmd == 'push':
1841 # Default to merging against our best guess of the upstream branch.
1842 args = [cl.GetUpstreamBranch()]
1843
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001844 if options.contributor:
1845 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1846 print "Please provide contibutor as 'First Last <email@example.com>'"
1847 return 1
1848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001850 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851
ukai@chromium.org259e4682012-10-25 07:36:33 +00001852 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853 return 1
1854
1855 # This rev-list syntax means "show all commits not in my branch that
1856 # are in base_branch".
1857 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1858 base_branch]).splitlines()
1859 if upstream_commits:
1860 print ('Base branch "%s" has %d commits '
1861 'not in this branch.' % (base_branch, len(upstream_commits)))
1862 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1863 return 1
1864
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001865 # This is the revision `svn dcommit` will commit on top of.
1866 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1867 '--pretty=format:%H'])
1868
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001869 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001870 # If the base_head is a submodule merge commit, the first parent of the
1871 # base_head should be a git-svn commit, which is what we're interested in.
1872 base_svn_head = base_branch
1873 if base_has_submodules:
1874 base_svn_head += '^1'
1875
1876 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001877 if extra_commits:
1878 print ('This branch has %d additional commits not upstreamed yet.'
1879 % len(extra_commits.splitlines()))
1880 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1881 'before attempting to %s.' % (base_branch, cmd))
1882 return 1
1883
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001884 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001885 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001886 author = None
1887 if options.contributor:
1888 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001889 hook_results = cl.RunHook(
1890 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001891 may_prompt=not options.force,
1892 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001893 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001894 if not hook_results.should_continue():
1895 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896
1897 if cmd == 'dcommit':
1898 # Check the tree status if the tree status URL is set.
1899 status = GetTreeStatus()
1900 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001901 print('The tree is closed. Please wait for it to reopen. Use '
1902 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 return 1
1904 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001905 print('Unable to determine tree status. Please verify manually and '
1906 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001907 else:
1908 breakpad.SendStack(
1909 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001910 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1911 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001912 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001914 change_desc = ChangeDescription(options.message)
1915 if not change_desc.description and cl.GetIssue():
1916 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001917
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001918 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001919 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001920 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001921 else:
1922 print 'No description set.'
1923 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1924 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001926 # Keep a separate copy for the commit message, because the commit message
1927 # contains the link to the Rietveld issue, while the Rietveld message contains
1928 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001929 # Keep a separate copy for the commit message.
1930 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001931 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001932
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001933 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001934 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001935 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001937 commit_desc.append_footer('Patch from %s.' % options.contributor)
1938
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001939 print('Description:')
1940 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941
1942 branches = [base_branch, cl.GetBranchRef()]
1943 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001944 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001946 # We want to squash all this branch's commits into one commit with the proper
1947 # description. We do this by doing a "reset --soft" to the base branch (which
1948 # keeps the working copy the same), then dcommitting that. If origin/master
1949 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1950 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001952 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1953 # Delete the branches if they exist.
1954 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1955 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1956 result = RunGitWithCode(showref_cmd)
1957 if result[0] == 0:
1958 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959
1960 # We might be in a directory that's present in this branch but not in the
1961 # trunk. Move up to the top of the tree so that git commands that expect a
1962 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001963 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964 if rel_base_path:
1965 os.chdir(rel_base_path)
1966
1967 # Stuff our change into the merge branch.
1968 # We wrap in a try...finally block so if anything goes wrong,
1969 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001970 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001972 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1973 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001975 RunGit(
1976 [
1977 'commit', '--author', options.contributor,
1978 '-m', commit_desc.description,
1979 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001980 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001981 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001982 if base_has_submodules:
1983 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1984 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1985 RunGit(['checkout', CHERRY_PICK_BRANCH])
1986 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987 if cmd == 'push':
1988 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001989 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990 retcode, output = RunGitWithCode(
1991 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1992 logging.debug(output)
1993 else:
1994 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001995 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001996 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001997 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998 finally:
1999 # And then swap back to the original branch and clean up.
2000 RunGit(['checkout', '-q', cl.GetBranch()])
2001 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002002 if base_has_submodules:
2003 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004
2005 if cl.GetIssue():
2006 if cmd == 'dcommit' and 'Committed r' in output:
2007 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2008 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00002009 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
2010 for l in output.splitlines(False))
2011 match = filter(None, match)
2012 if len(match) != 1:
2013 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
2014 output)
2015 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016 else:
2017 return 1
2018 viewvc_url = settings.GetViewVCUrl()
2019 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002020 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00002021 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002022 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 print ('Closing issue '
2024 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002025 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002027 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002028 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00002029 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002030 if options.bypass_hooks:
2031 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2032 else:
2033 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002034 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002035 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002036
2037 if retcode == 0:
2038 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2039 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002040 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042 return 0
2043
2044
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002045@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002047 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002049 message = """This doesn't appear to be an SVN repository.
2050If your project has a git mirror with an upstream SVN master, you probably need
2051to run 'git svn init', see your project's git mirror documentation.
2052If your project has a true writeable upstream repository, you probably want
2053to run 'git cl push' instead.
2054Choose wisely, if you get this wrong, your commit might appear to succeed but
2055will instead be silently ignored."""
2056 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002057 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058 return SendUpstream(parser, args, 'dcommit')
2059
2060
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002061@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002062def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002063 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064 if settings.GetIsGitSvn():
2065 print('This appears to be an SVN repository.')
2066 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002067 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 return SendUpstream(parser, args, 'push')
2069
2070
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002071@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002073 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002074 parser.add_option('-b', dest='newbranch',
2075 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002076 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002078 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2079 help='Change to the directory DIR immediately, '
2080 'before doing anything else.')
2081 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002082 help='failed patches spew .rej files rather than '
2083 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2085 help="don't commit after patch applies")
2086 (options, args) = parser.parse_args(args)
2087 if len(args) != 1:
2088 parser.print_help()
2089 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002090 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002092 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002093 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002094
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002095 if options.newbranch:
2096 if options.force:
2097 RunGit(['branch', '-D', options.newbranch],
2098 stderr=subprocess2.PIPE, error_ok=True)
2099 RunGit(['checkout', '-b', options.newbranch,
2100 Changelist().GetUpstreamBranch()])
2101
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002102 return PatchIssue(issue_arg, options.reject, options.nocommit,
2103 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002104
2105
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002106def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002107 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002109 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002110 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002111 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002112 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002114 # Assume it's a URL to the patch. Default to https.
2115 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002116 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002117 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 DieWithError('Must pass an issue ID or full URL for '
2119 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002120 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002121 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002122 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124 # Switch up to the top-level directory, if necessary, in preparation for
2125 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002126 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 if top:
2128 os.chdir(top)
2129
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130 # Git patches have a/ at the beginning of source paths. We strip that out
2131 # with a sed script rather than the -p flag to patch so we can feed either
2132 # Git or svn-style patches into the same apply command.
2133 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002134 try:
2135 patch_data = subprocess2.check_output(
2136 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2137 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 DieWithError('Git patch mungling failed.')
2139 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002140
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141 # We use "git apply" to apply the patch instead of "patch" so that we can
2142 # pick up file adds.
2143 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002144 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002145 if directory:
2146 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002147 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002149 elif IsGitVersionAtLeast('1.7.12'):
2150 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002151 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002152 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002153 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002154 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002155 DieWithError('Failed to apply the patch')
2156
2157 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002158 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002159 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2160 cl = Changelist()
2161 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002162 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002163 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002164 else:
2165 print "Patch applied to index."
2166 return 0
2167
2168
2169def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002170 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002171 # Provide a wrapper for git svn rebase to help avoid accidental
2172 # git svn dcommit.
2173 # It's the only command that doesn't use parser at all since we just defer
2174 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002175
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002176 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002177
2178
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002179def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002180 """Fetches the tree status and returns either 'open', 'closed',
2181 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002182 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183 if url:
2184 status = urllib2.urlopen(url).read().lower()
2185 if status.find('closed') != -1 or status == '0':
2186 return 'closed'
2187 elif status.find('open') != -1 or status == '1':
2188 return 'open'
2189 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190 return 'unset'
2191
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002193def GetTreeStatusReason():
2194 """Fetches the tree status from a json url and returns the message
2195 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002196 url = settings.GetTreeStatusUrl()
2197 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002198 connection = urllib2.urlopen(json_url)
2199 status = json.loads(connection.read())
2200 connection.close()
2201 return status['message']
2202
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002203
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002204def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002205 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002206 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002207 status = GetTreeStatus()
2208 if 'unset' == status:
2209 print 'You must configure your tree status URL by running "git cl config".'
2210 return 2
2211
2212 print "The tree is %s" % status
2213 print
2214 print GetTreeStatusReason()
2215 if status != 'open':
2216 return 1
2217 return 0
2218
2219
maruel@chromium.org15192402012-09-06 12:38:29 +00002220def CMDtry(parser, args):
2221 """Triggers a try job through Rietveld."""
2222 group = optparse.OptionGroup(parser, "Try job options")
2223 group.add_option(
2224 "-b", "--bot", action="append",
2225 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2226 "times to specify multiple builders. ex: "
2227 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2228 "the try server waterfall for the builders name and the tests "
2229 "available. Can also be used to specify gtest_filter, e.g. "
2230 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2231 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002232 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002233 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002234 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002235 "-r", "--revision",
2236 help="Revision to use for the try job; default: the "
2237 "revision will be determined by the try server; see "
2238 "its waterfall for more info")
2239 group.add_option(
2240 "-c", "--clobber", action="store_true", default=False,
2241 help="Force a clobber before building; e.g. don't do an "
2242 "incremental build")
2243 group.add_option(
2244 "--project",
2245 help="Override which project to use. Projects are defined "
2246 "server-side to define what default bot set to use")
2247 group.add_option(
2248 "-t", "--testfilter", action="append", default=[],
2249 help=("Apply a testfilter to all the selected builders. Unless the "
2250 "builders configurations are similar, use multiple "
2251 "--bot <builder>:<test> arguments."))
2252 group.add_option(
2253 "-n", "--name", help="Try job name; default to current branch name")
2254 parser.add_option_group(group)
2255 options, args = parser.parse_args(args)
2256
2257 if args:
2258 parser.error('Unknown arguments: %s' % args)
2259
2260 cl = Changelist()
2261 if not cl.GetIssue():
2262 parser.error('Need to upload first')
2263
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002264 props = cl.GetIssueProperties()
2265 if props.get('private'):
2266 parser.error('Cannot use trybots with private issue')
2267
maruel@chromium.org15192402012-09-06 12:38:29 +00002268 if not options.name:
2269 options.name = cl.GetBranch()
2270
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002271 if options.bot and not options.master:
2272 parser.error('For manually specified bots please also specify the '
2273 'tryserver master, e.g. "-m tryserver.chromium".')
2274
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002275 def GetMasterMap():
2276 # Process --bot and --testfilter.
2277 if not options.bot:
2278 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002279
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002280 # Get try masters from PRESUBMIT.py files.
2281 masters = presubmit_support.DoGetTryMasters(
2282 change,
2283 change.LocalPaths(),
2284 settings.GetRoot(),
2285 None,
2286 None,
2287 options.verbose,
2288 sys.stdout)
2289 if masters:
2290 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002291
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002292 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2293 options.bot = presubmit_support.DoGetTrySlaves(
2294 change,
2295 change.LocalPaths(),
2296 settings.GetRoot(),
2297 None,
2298 None,
2299 options.verbose,
2300 sys.stdout)
2301 if not options.bot:
2302 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002303
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002304 builders_and_tests = {}
2305 # TODO(machenbach): The old style command-line options don't support
2306 # multiple try masters yet.
2307 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2308 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2309
2310 for bot in old_style:
2311 if ':' in bot:
2312 builder, tests = bot.split(':', 1)
2313 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2314 elif ',' in bot:
2315 parser.error('Specify one bot per --bot flag')
2316 else:
2317 builders_and_tests.setdefault(bot, []).append('defaulttests')
2318
2319 for bot, tests in new_style:
2320 builders_and_tests.setdefault(bot, []).extend(tests)
2321
2322 # Return a master map with one master to be backwards compatible. The
2323 # master name defaults to an empty string, which will cause the master
2324 # not to be set on rietveld (deprecated).
2325 return {options.master: builders_and_tests}
2326
2327 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002328
maruel@chromium.org15192402012-09-06 12:38:29 +00002329 if options.testfilter:
2330 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002331 masters = dict((master, dict(
2332 (b, forced_tests) for b, t in slaves.iteritems()
2333 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002334
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002335 for builders in masters.itervalues():
2336 if any('triggered' in b for b in builders):
2337 print >> sys.stderr, (
2338 'ERROR You are trying to send a job to a triggered bot. This type of'
2339 ' bot requires an\ninitial job from a parent (usually a builder). '
2340 'Instead send your job to the parent.\n'
2341 'Bot list: %s' % builders)
2342 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002343
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002344 patchset = cl.GetMostRecentPatchset()
2345 if patchset and patchset != cl.GetPatchset():
2346 print(
2347 '\nWARNING Mismatch between local config and server. Did a previous '
2348 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2349 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002350 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002351 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002352 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002353 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002354 except urllib2.HTTPError, e:
2355 if e.code == 404:
2356 print('404 from rietveld; '
2357 'did you mean to use "git try" instead of "git cl try"?')
2358 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002359 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002360
2361 for (master, builders) in masters.iteritems():
2362 if master:
2363 print 'Master: %s' % master
2364 length = max(len(builder) for builder in builders)
2365 for builder in sorted(builders):
2366 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002367 return 0
2368
2369
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002370@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002371def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002372 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002373 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002374 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002375 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002377 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002378 if args:
2379 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002380 branch = cl.GetBranch()
2381 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002382 cl = Changelist()
2383 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002384
2385 # Clear configured merge-base, if there is one.
2386 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002387 else:
2388 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002389 return 0
2390
2391
thestig@chromium.org00858c82013-12-02 23:08:03 +00002392def CMDweb(parser, args):
2393 """Opens the current CL in the web browser."""
2394 _, args = parser.parse_args(args)
2395 if args:
2396 parser.error('Unrecognized args: %s' % ' '.join(args))
2397
2398 issue_url = Changelist().GetIssueURL()
2399 if not issue_url:
2400 print >> sys.stderr, 'ERROR No issue to open'
2401 return 1
2402
2403 webbrowser.open(issue_url)
2404 return 0
2405
2406
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002407def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002408 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002409 _, args = parser.parse_args(args)
2410 if args:
2411 parser.error('Unrecognized args: %s' % ' '.join(args))
2412 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002413 props = cl.GetIssueProperties()
2414 if props.get('private'):
2415 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002416 cl.SetFlag('commit', '1')
2417 return 0
2418
2419
groby@chromium.org411034a2013-02-26 15:12:01 +00002420def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002421 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002422 _, args = parser.parse_args(args)
2423 if args:
2424 parser.error('Unrecognized args: %s' % ' '.join(args))
2425 cl = Changelist()
2426 # Ensure there actually is an issue to close.
2427 cl.GetDescription()
2428 cl.CloseIssue()
2429 return 0
2430
2431
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002432def CMDdiff(parser, args):
2433 """shows differences between local tree and last upload."""
2434 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002435 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002436 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002437 if not issue:
2438 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002439 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002440 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002441
2442 # Create a new branch based on the merge-base
2443 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2444 try:
2445 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002446 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002447 if rtn != 0:
2448 return rtn
2449
2450 # Switch back to starting brand and diff against the temporary
2451 # branch containing the latest rietveld patch.
2452 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2453 finally:
2454 RunGit(['checkout', '-q', branch])
2455 RunGit(['branch', '-D', TMP_BRANCH])
2456
2457 return 0
2458
2459
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002460def CMDowners(parser, args):
2461 """interactively find the owners for reviewing"""
2462 parser.add_option(
2463 '--no-color',
2464 action='store_true',
2465 help='Use this option to disable color output')
2466 options, args = parser.parse_args(args)
2467
2468 author = RunGit(['config', 'user.email']).strip() or None
2469
2470 cl = Changelist()
2471
2472 if args:
2473 if len(args) > 1:
2474 parser.error('Unknown args')
2475 base_branch = args[0]
2476 else:
2477 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002478 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002479
2480 change = cl.GetChange(base_branch, None)
2481 return owners_finder.OwnersFinder(
2482 [f.LocalPath() for f in
2483 cl.GetChange(base_branch, None).AffectedFiles()],
2484 change.RepositoryRoot(), author,
2485 fopen=file, os_path=os.path, glob=glob.glob,
2486 disable_color=options.no_color).run()
2487
2488
enne@chromium.org555cfe42014-01-29 18:21:39 +00002489@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002490def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002491 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002492 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002493 parser.add_option('--full', action='store_true',
2494 help='Reformat the full content of all touched files')
2495 parser.add_option('--dry-run', action='store_true',
2496 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002497 parser.add_option('--diff', action='store_true',
2498 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002499 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002500
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002501 # git diff generates paths against the root of the repository. Change
2502 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002503 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002504 if rel_base_path:
2505 os.chdir(rel_base_path)
2506
digit@chromium.org29e47272013-05-17 17:01:46 +00002507 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002508 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002509 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002510 # Only list the names of modified files.
2511 diff_cmd.append('--name-only')
2512 else:
2513 # Only generate context-less patches.
2514 diff_cmd.append('-U0')
2515
2516 # Grab the merge-base commit, i.e. the upstream commit of the current
2517 # branch when it was created or the last time it was rebased. This is
2518 # to cover the case where the user may have called "git fetch origin",
2519 # moving the origin branch to a newer commit, but hasn't rebased yet.
2520 upstream_commit = None
2521 cl = Changelist()
2522 upstream_branch = cl.GetUpstreamBranch()
2523 if upstream_branch:
2524 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2525 upstream_commit = upstream_commit.strip()
2526
2527 if not upstream_commit:
2528 DieWithError('Could not find base commit for this branch. '
2529 'Are you in detached state?')
2530
2531 diff_cmd.append(upstream_commit)
2532
2533 # Handle source file filtering.
2534 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002535 if args:
2536 for arg in args:
2537 if os.path.isdir(arg):
2538 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2539 elif os.path.isfile(arg):
2540 diff_cmd.append(arg)
2541 else:
2542 DieWithError('Argument "%s" is not a file or a directory' % arg)
2543 else:
2544 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002545 diff_output = RunGit(diff_cmd)
2546
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002547 top_dir = os.path.normpath(
2548 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2549
2550 # Locate the clang-format binary in the checkout
2551 try:
2552 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2553 except clang_format.NotFoundError, e:
2554 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002555
digit@chromium.org29e47272013-05-17 17:01:46 +00002556 if opts.full:
2557 # diff_output is a list of files to send to clang-format.
2558 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002559 if not files:
2560 print "Nothing to format."
2561 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002562 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002563 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002564 cmd.append('-i')
2565 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002566 if opts.diff:
2567 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002568 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002569 env = os.environ.copy()
2570 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002571 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002572 try:
2573 script = clang_format.FindClangFormatScriptInChromiumTree(
2574 'clang-format-diff.py')
2575 except clang_format.NotFoundError, e:
2576 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002577
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002578 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002579 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002580 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002581
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002582 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002583 if opts.diff:
2584 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002585 if opts.dry_run and len(stdout) > 0:
2586 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002587
2588 return 0
2589
2590
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002591class OptionParser(optparse.OptionParser):
2592 """Creates the option parse and add --verbose support."""
2593 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002594 optparse.OptionParser.__init__(
2595 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002596 self.add_option(
2597 '-v', '--verbose', action='count', default=0,
2598 help='Use 2 times for more debugging info')
2599
2600 def parse_args(self, args=None, values=None):
2601 options, args = optparse.OptionParser.parse_args(self, args, values)
2602 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2603 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2604 return options, args
2605
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002607def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002608 if sys.hexversion < 0x02060000:
2609 print >> sys.stderr, (
2610 '\nYour python version %s is unsupported, please upgrade.\n' %
2611 sys.version.split(' ', 1)[0])
2612 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002613
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002614 # Reload settings.
2615 global settings
2616 settings = Settings()
2617
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002618 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002619 dispatcher = subcommand.CommandDispatcher(__name__)
2620 try:
2621 return dispatcher.execute(OptionParser(), argv)
2622 except urllib2.HTTPError, e:
2623 if e.code != 500:
2624 raise
2625 DieWithError(
2626 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2627 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002628
2629
2630if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002631 # These affect sys.stdout so do it outside of main() to simplify mocks in
2632 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002633 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002634 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002635 sys.exit(main(sys.argv[1:]))