blob: 82e9208632860decd8ad16c546eb57e433f6482e [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()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000699 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000700 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)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000713 except urllib2.URLError as e:
714 print >> sys.stderr, (
715 'Warning: Failed to retrieve CL description due to network '
716 'failure.')
717 self.description = ''
718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 self.has_description = True
720 if pretty:
721 wrapper = textwrap.TextWrapper()
722 wrapper.initial_indent = wrapper.subsequent_indent = ' '
723 return wrapper.fill(self.description)
724 return self.description
725
726 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000727 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000728 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 patchset = RunGit(['config', self._PatchsetSetting()],
730 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000731 self.patchset = int(patchset) or None if patchset else None
732 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 return self.patchset
734
735 def SetPatchset(self, patchset):
736 """Set this branch's patchset. If patchset=0, clears the patchset."""
737 if patchset:
738 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000739 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 else:
741 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000742 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000743 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000745 def GetMostRecentPatchset(self):
746 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000747
748 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000749 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000750 '/download/issue%s_%s.diff' % (issue, patchset))
751
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000752 def GetIssueProperties(self):
753 if self._props is None:
754 issue = self.GetIssue()
755 if not issue:
756 self._props = {}
757 else:
758 self._props = self.RpcServer().get_issue_properties(issue, True)
759 return self._props
760
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000761 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000762 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000763
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 def SetIssue(self, issue):
765 """Set this branch's issue. If issue=0, clears the issue."""
766 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000767 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 RunGit(['config', self._IssueSetting(), str(issue)])
769 if self.rietveld_server:
770 RunGit(['config', self._RietveldServer(), self.rietveld_server])
771 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000772 current_issue = self.GetIssue()
773 if current_issue:
774 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000775 self.issue = None
776 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000778 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000779 if not self.GitSanityChecks(upstream_branch):
780 DieWithError('\nGit sanity check failure')
781
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000782 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000783 if not root:
784 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000785 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000786
787 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000788 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000789 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000790 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000791 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000792 except subprocess2.CalledProcessError:
793 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000794 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000795 'This branch probably doesn\'t exist anymore. To reset the\n'
796 'tracking branch, please run\n'
797 ' git branch --set-upstream %s trunk\n'
798 'replacing trunk with origin/master or the relevant branch') %
799 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000800
maruel@chromium.org52424302012-08-29 15:14:30 +0000801 issue = self.GetIssue()
802 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000803 if issue:
804 description = self.GetDescription()
805 else:
806 # If the change was never uploaded, use the log messages of all commits
807 # up to the branch point, as git cl upload will prefill the description
808 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000809 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
810 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000811
812 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000813 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000814 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000815 name,
816 description,
817 absroot,
818 files,
819 issue,
820 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000821 author,
822 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000823
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000824 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000825 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000826
827 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000828 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000829 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000830 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000831 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000832 except presubmit_support.PresubmitFailure, e:
833 DieWithError(
834 ('%s\nMaybe your depot_tools is out of date?\n'
835 'If all fails, contact maruel@') % e)
836
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000837 def UpdateDescription(self, description):
838 self.description = description
839 return self.RpcServer().update_description(
840 self.GetIssue(), self.description)
841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000843 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000844 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000846 def SetFlag(self, flag, value):
847 """Patchset must match."""
848 if not self.GetPatchset():
849 DieWithError('The patchset needs to match. Send another patchset.')
850 try:
851 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000852 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000853 except urllib2.HTTPError, e:
854 if e.code == 404:
855 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
856 if e.code == 403:
857 DieWithError(
858 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
859 'match?') % (self.GetIssue(), self.GetPatchset()))
860 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000862 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 """Returns an upload.RpcServer() to access this review's rietveld instance.
864 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000865 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000866 self._rpc_server = rietveld.CachingRietveld(
867 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000868 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869
870 def _IssueSetting(self):
871 """Return the git setting that stores this change's issue."""
872 return 'branch.%s.rietveldissue' % self.GetBranch()
873
874 def _PatchsetSetting(self):
875 """Return the git setting that stores this change's most recent patchset."""
876 return 'branch.%s.rietveldpatchset' % self.GetBranch()
877
878 def _RietveldServer(self):
879 """Returns the git setting that stores this change's rietveld server."""
880 return 'branch.%s.rietveldserver' % self.GetBranch()
881
882
883def GetCodereviewSettingsInteractively():
884 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000885 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886 server = settings.GetDefaultServerUrl(error_ok=True)
887 prompt = 'Rietveld server (host[:port])'
888 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000889 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890 if not server and not newserver:
891 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000892 if newserver:
893 newserver = gclient_utils.UpgradeToHttps(newserver)
894 if newserver != server:
895 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000897 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 prompt = caption
899 if initial:
900 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000901 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902 if new_val == 'x':
903 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000904 elif new_val:
905 if is_url:
906 new_val = gclient_utils.UpgradeToHttps(new_val)
907 if new_val != initial:
908 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000910 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000911 SetProperty(settings.GetDefaultPrivateFlag(),
912 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000914 'tree-status-url', False)
915 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000916 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917
918 # TODO: configure a default branch to diff against, rather than this
919 # svn-based hackery.
920
921
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000922class ChangeDescription(object):
923 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000924 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000925 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000926
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000927 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000928 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000929
agable@chromium.org42c20792013-09-12 17:34:49 +0000930 @property # www.logilab.org/ticket/89786
931 def description(self): # pylint: disable=E0202
932 return '\n'.join(self._description_lines)
933
934 def set_description(self, desc):
935 if isinstance(desc, basestring):
936 lines = desc.splitlines()
937 else:
938 lines = [line.rstrip() for line in desc]
939 while lines and not lines[0]:
940 lines.pop(0)
941 while lines and not lines[-1]:
942 lines.pop(-1)
943 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000944
945 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000946 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000947 assert isinstance(reviewers, list), reviewers
948 if not reviewers:
949 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000950 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000951
agable@chromium.org42c20792013-09-12 17:34:49 +0000952 # Get the set of R= and TBR= lines and remove them from the desciption.
953 regexp = re.compile(self.R_LINE)
954 matches = [regexp.match(line) for line in self._description_lines]
955 new_desc = [l for i, l in enumerate(self._description_lines)
956 if not matches[i]]
957 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000958
agable@chromium.org42c20792013-09-12 17:34:49 +0000959 # Construct new unified R= and TBR= lines.
960 r_names = []
961 tbr_names = []
962 for match in matches:
963 if not match:
964 continue
965 people = cleanup_list([match.group(2).strip()])
966 if match.group(1) == 'TBR':
967 tbr_names.extend(people)
968 else:
969 r_names.extend(people)
970 for name in r_names:
971 if name not in reviewers:
972 reviewers.append(name)
973 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
974 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
975
976 # Put the new lines in the description where the old first R= line was.
977 line_loc = next((i for i, match in enumerate(matches) if match), -1)
978 if 0 <= line_loc < len(self._description_lines):
979 if new_tbr_line:
980 self._description_lines.insert(line_loc, new_tbr_line)
981 if new_r_line:
982 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000983 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000984 if new_r_line:
985 self.append_footer(new_r_line)
986 if new_tbr_line:
987 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000988
989 def prompt(self):
990 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000991 self.set_description([
992 '# Enter a description of the change.',
993 '# This will be displayed on the codereview site.',
994 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000995 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000996 '--------------------',
997 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000998
agable@chromium.org42c20792013-09-12 17:34:49 +0000999 regexp = re.compile(self.BUG_LINE)
1000 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001001 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001002 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001003 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001004 if not content:
1005 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001006 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001007
1008 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001009 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1010 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001011 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001012 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001013
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001014 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001015 if self._description_lines:
1016 # Add an empty line if either the last line or the new line isn't a tag.
1017 last_line = self._description_lines[-1]
1018 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1019 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1020 self._description_lines.append('')
1021 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001022
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001023 def get_reviewers(self):
1024 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001025 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1026 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001027 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001028
1029
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001030def get_approving_reviewers(props):
1031 """Retrieves the reviewers that approved a CL from the issue properties with
1032 messages.
1033
1034 Note that the list may contain reviewers that are not committer, thus are not
1035 considered by the CQ.
1036 """
1037 return sorted(
1038 set(
1039 message['sender']
1040 for message in props['messages']
1041 if message['approval'] and message['sender'] in props['reviewers']
1042 )
1043 )
1044
1045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046def FindCodereviewSettingsFile(filename='codereview.settings'):
1047 """Finds the given file starting in the cwd and going up.
1048
1049 Only looks up to the top of the repository unless an
1050 'inherit-review-settings-ok' file exists in the root of the repository.
1051 """
1052 inherit_ok_file = 'inherit-review-settings-ok'
1053 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001054 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1056 root = '/'
1057 while True:
1058 if filename in os.listdir(cwd):
1059 if os.path.isfile(os.path.join(cwd, filename)):
1060 return open(os.path.join(cwd, filename))
1061 if cwd == root:
1062 break
1063 cwd = os.path.dirname(cwd)
1064
1065
1066def LoadCodereviewSettingsFromFile(fileobj):
1067 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001068 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070 def SetProperty(name, setting, unset_error_ok=False):
1071 fullname = 'rietveld.' + name
1072 if setting in keyvals:
1073 RunGit(['config', fullname, keyvals[setting]])
1074 else:
1075 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1076
1077 SetProperty('server', 'CODE_REVIEW_SERVER')
1078 # Only server setting is required. Other settings can be absent.
1079 # In that case, we ignore errors raised during option deletion attempt.
1080 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001081 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1083 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001084 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001085 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1086 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001087 SetProperty('project', 'PROJECT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001089 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001090 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1093 #should be of the form
1094 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1095 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1096 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1097 keyvals['ORIGIN_URL_CONFIG']])
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001100def urlretrieve(source, destination):
1101 """urllib is broken for SSL connections via a proxy therefore we
1102 can't use urllib.urlretrieve()."""
1103 with open(destination, 'w') as f:
1104 f.write(urllib2.urlopen(source).read())
1105
1106
ukai@chromium.org712d6102013-11-27 00:52:58 +00001107def hasSheBang(fname):
1108 """Checks fname is a #! script."""
1109 with open(fname) as f:
1110 return f.read(2).startswith('#!')
1111
1112
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001113def DownloadHooks(force):
1114 """downloads hooks
1115
1116 Args:
1117 force: True to update hooks. False to install hooks if not present.
1118 """
1119 if not settings.GetIsGerrit():
1120 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001121 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001122 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1123 if not os.access(dst, os.X_OK):
1124 if os.path.exists(dst):
1125 if not force:
1126 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001127 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001128 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001129 if not hasSheBang(dst):
1130 DieWithError('Not a script: %s\n'
1131 'You need to download from\n%s\n'
1132 'into .git/hooks/commit-msg and '
1133 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001134 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1135 except Exception:
1136 if os.path.exists(dst):
1137 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001138 DieWithError('\nFailed to download hooks.\n'
1139 'You need to download from\n%s\n'
1140 'into .git/hooks/commit-msg and '
1141 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001142
1143
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001144@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001146 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001148 parser.add_option('--activate-update', action='store_true',
1149 help='activate auto-updating [rietveld] section in '
1150 '.git/config')
1151 parser.add_option('--deactivate-update', action='store_true',
1152 help='deactivate auto-updating [rietveld] section in '
1153 '.git/config')
1154 options, args = parser.parse_args(args)
1155
1156 if options.deactivate_update:
1157 RunGit(['config', 'rietveld.autoupdate', 'false'])
1158 return
1159
1160 if options.activate_update:
1161 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1162 return
1163
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 if len(args) == 0:
1165 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001166 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167 return 0
1168
1169 url = args[0]
1170 if not url.endswith('codereview.settings'):
1171 url = os.path.join(url, 'codereview.settings')
1172
1173 # Load code review settings and download hooks (if available).
1174 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001175 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 return 0
1177
1178
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001179def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001180 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001181 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1182 branch = ShortBranchName(branchref)
1183 _, args = parser.parse_args(args)
1184 if not args:
1185 print("Current base-url:")
1186 return RunGit(['config', 'branch.%s.base-url' % branch],
1187 error_ok=False).strip()
1188 else:
1189 print("Setting base-url to %s" % args[0])
1190 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1191 error_ok=False).strip()
1192
1193
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001195 """Show status of changelists.
1196
1197 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001198 - Red not sent for review or broken
1199 - Blue waiting for review
1200 - Yellow waiting for you to reply to review
1201 - Green LGTM'ed
1202 - Magenta in the commit queue
1203 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001204
1205 Also see 'git cl comments'.
1206 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 parser.add_option('--field',
1208 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001209 parser.add_option('-f', '--fast', action='store_true',
1210 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001212 if args:
1213 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001216 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 if options.field.startswith('desc'):
1218 print cl.GetDescription()
1219 elif options.field == 'id':
1220 issueid = cl.GetIssue()
1221 if issueid:
1222 print issueid
1223 elif options.field == 'patch':
1224 patchset = cl.GetPatchset()
1225 if patchset:
1226 print patchset
1227 elif options.field == 'url':
1228 url = cl.GetIssueURL()
1229 if url:
1230 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001231 return 0
1232
1233 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1234 if not branches:
1235 print('No local branch found.')
1236 return 0
1237
1238 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001239 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001240 alignment = max(5, max(len(b) for b in branches))
1241 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001242 # Adhoc thread pool to request data concurrently.
1243 output = Queue.Queue()
1244
1245 # Silence upload.py otherwise it becomes unweldly.
1246 upload.verbosity = 0
1247
1248 if not options.fast:
1249 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001250 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001251 c = Changelist(branchref=b)
1252 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001253 props = {}
1254 r = None
1255 if i:
1256 try:
1257 props = c.GetIssueProperties()
1258 r = c.GetApprovingReviewers() if i else None
1259 except urllib2.HTTPError:
1260 # The issue probably doesn't exist anymore.
1261 i += ' (broken)'
1262
1263 msgs = props.get('messages') or []
1264
1265 if not i:
1266 color = Fore.WHITE
1267 elif props.get('closed'):
1268 # Issue is closed.
1269 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001270 elif props.get('commit'):
1271 # Issue is in the commit queue.
1272 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001273 elif r:
1274 # Was LGTM'ed.
1275 color = Fore.GREEN
1276 elif not msgs:
1277 # No message was sent.
1278 color = Fore.RED
1279 elif msgs[-1]['sender'] != props.get('owner_email'):
1280 color = Fore.YELLOW
1281 else:
1282 color = Fore.BLUE
1283 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001284
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001285 # Process one branch synchronously to work through authentication, then
1286 # spawn threads to process all the other branches in parallel.
1287 if branches:
1288 fetch(branches[0])
1289 threads = [
1290 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001291 for t in threads:
1292 t.daemon = True
1293 t.start()
1294 else:
1295 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1296 for b in branches:
1297 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001298 url = c.GetIssueURL()
1299 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001300
1301 tmp = {}
1302 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001303 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001304 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001305 b, i, color = output.get()
1306 tmp[b] = (i, color)
1307 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001308 reset = Fore.RESET
1309 if not sys.stdout.isatty():
1310 color = ''
1311 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001312 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001313 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001314
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001315 cl = Changelist()
1316 print
1317 print 'Current branch:',
1318 if not cl.GetIssue():
1319 print 'no issue assigned.'
1320 return 0
1321 print cl.GetBranch()
1322 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001323 if not options.fast:
1324 print 'Issue description:'
1325 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326 return 0
1327
1328
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001329def colorize_CMDstatus_doc():
1330 """To be called once in main() to add colors to git cl status help."""
1331 colors = [i for i in dir(Fore) if i[0].isupper()]
1332
1333 def colorize_line(line):
1334 for color in colors:
1335 if color in line.upper():
1336 # Extract whitespaces first and the leading '-'.
1337 indent = len(line) - len(line.lstrip(' ')) + 1
1338 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1339 return line
1340
1341 lines = CMDstatus.__doc__.splitlines()
1342 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1343
1344
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001345@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001347 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348
1349 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001350 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001351 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352
1353 cl = Changelist()
1354 if len(args) > 0:
1355 try:
1356 issue = int(args[0])
1357 except ValueError:
1358 DieWithError('Pass a number to set the issue or none to list it.\n'
1359 'Maybe you want to run git cl status?')
1360 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001361 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 return 0
1363
1364
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001365def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001366 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001367 (_, args) = parser.parse_args(args)
1368 if args:
1369 parser.error('Unsupported argument: %s' % args)
1370
1371 cl = Changelist()
1372 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001373 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001374 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001375 if message['disapproval']:
1376 color = Fore.RED
1377 elif message['approval']:
1378 color = Fore.GREEN
1379 elif message['sender'] == data['owner_email']:
1380 color = Fore.MAGENTA
1381 else:
1382 color = Fore.BLUE
1383 print '\n%s%s %s%s' % (
1384 color, message['date'].split('.', 1)[0], message['sender'],
1385 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001386 if message['text'].strip():
1387 print '\n'.join(' ' + l for l in message['text'].splitlines())
1388 return 0
1389
1390
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001391def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001392 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001393 cl = Changelist()
1394 if not cl.GetIssue():
1395 DieWithError('This branch has no associated changelist.')
1396 description = ChangeDescription(cl.GetDescription())
1397 description.prompt()
1398 cl.UpdateDescription(description.description)
1399 return 0
1400
1401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402def CreateDescriptionFromLog(args):
1403 """Pulls out the commit log to use as a base for the CL description."""
1404 log_args = []
1405 if len(args) == 1 and not args[0].endswith('.'):
1406 log_args = [args[0] + '..']
1407 elif len(args) == 1 and args[0].endswith('...'):
1408 log_args = [args[0][:-1]]
1409 elif len(args) == 2:
1410 log_args = [args[0] + '..' + args[1]]
1411 else:
1412 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001413 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
1415
thestig@chromium.org44202a22014-03-11 19:22:18 +00001416def CMDlint(parser, args):
1417 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001418 parser.add_option('--filter', action='append', metavar='-x,+y',
1419 help='Comma-separated list of cpplint\'s category-filters')
1420 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001421
1422 # Access to a protected member _XX of a client class
1423 # pylint: disable=W0212
1424 try:
1425 import cpplint
1426 import cpplint_chromium
1427 except ImportError:
1428 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1429 return 1
1430
1431 # Change the current working directory before calling lint so that it
1432 # shows the correct base.
1433 previous_cwd = os.getcwd()
1434 os.chdir(settings.GetRoot())
1435 try:
1436 cl = Changelist()
1437 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1438 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001439 if not files:
1440 print "Cannot lint an empty CL"
1441 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001442
1443 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001444 command = args + files
1445 if options.filter:
1446 command = ['--filter=' + ','.join(options.filter)] + command
1447 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001448
1449 white_regex = re.compile(settings.GetLintRegex())
1450 black_regex = re.compile(settings.GetLintIgnoreRegex())
1451 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1452 for filename in filenames:
1453 if white_regex.match(filename):
1454 if black_regex.match(filename):
1455 print "Ignoring file %s" % filename
1456 else:
1457 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1458 extra_check_functions)
1459 else:
1460 print "Skipping file %s" % filename
1461 finally:
1462 os.chdir(previous_cwd)
1463 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1464 if cpplint._cpplint_state.error_count != 0:
1465 return 1
1466 return 0
1467
1468
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001470 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001471 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001473 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001474 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 (options, args) = parser.parse_args(args)
1476
ukai@chromium.org259e4682012-10-25 07:36:33 +00001477 if not options.force and is_dirty_git_tree('presubmit'):
1478 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 return 1
1480
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001481 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482 if args:
1483 base_branch = args[0]
1484 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001485 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001486 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001488 cl.RunHook(
1489 committing=not options.upload,
1490 may_prompt=False,
1491 verbose=options.verbose,
1492 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001493 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001494
1495
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001496def AddChangeIdToCommitMessage(options, args):
1497 """Re-commits using the current message, assumes the commit hook is in
1498 place.
1499 """
1500 log_desc = options.message or CreateDescriptionFromLog(args)
1501 git_command = ['commit', '--amend', '-m', log_desc]
1502 RunGit(git_command)
1503 new_log_desc = CreateDescriptionFromLog(args)
1504 if CHANGE_ID in new_log_desc:
1505 print 'git-cl: Added Change-Id to commit message.'
1506 else:
1507 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1508
1509
ukai@chromium.orge8077812012-02-03 03:41:46 +00001510def GerritUpload(options, args, cl):
1511 """upload the current branch to gerrit."""
1512 # We assume the remote called "origin" is the one we want.
1513 # It is probably not worthwhile to support different workflows.
1514 remote = 'origin'
1515 branch = 'master'
1516 if options.target_branch:
1517 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001519 change_desc = ChangeDescription(
1520 options.message or CreateDescriptionFromLog(args))
1521 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001522 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001523 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001524 if CHANGE_ID not in change_desc.description:
1525 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001526
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001527 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001528 if len(commits) > 1:
1529 print('WARNING: This will upload %d commits. Run the following command '
1530 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001531 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001532 print('You can also use `git squash-branch` to squash these into a single'
1533 'commit.')
1534 ask_for_data('About to upload; enter to confirm.')
1535
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001536 if options.reviewers:
1537 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001538
ukai@chromium.orge8077812012-02-03 03:41:46 +00001539 receive_options = []
1540 cc = cl.GetCCList().split(',')
1541 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001542 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001543 cc = filter(None, cc)
1544 if cc:
1545 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001546 if change_desc.get_reviewers():
1547 receive_options.extend(
1548 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001549
ukai@chromium.orge8077812012-02-03 03:41:46 +00001550 git_command = ['push']
1551 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001552 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001553 ' '.join(receive_options))
1554 git_command += [remote, 'HEAD:refs/for/' + branch]
1555 RunGit(git_command)
1556 # TODO(ukai): parse Change-Id: and set issue number?
1557 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001558
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001560def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001561 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1563 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564 if options.emulate_svn_auto_props:
1565 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566
1567 change_desc = None
1568
pgervais@chromium.org91141372014-01-09 23:27:20 +00001569 if options.email is not None:
1570 upload_args.extend(['--email', options.email])
1571
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001573 if options.title:
1574 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001575 if options.message:
1576 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001577 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578 print ("This branch is associated with issue %s. "
1579 "Adding patch to that issue." % cl.GetIssue())
1580 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001581 if options.title:
1582 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001583 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001584 change_desc = ChangeDescription(message)
1585 if options.reviewers:
1586 change_desc.update_reviewers(options.reviewers)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001587 if options.auto_bots:
1588 masters = presubmit_support.DoGetTryMasters(
1589 change,
1590 change.LocalPaths(),
1591 settings.GetRoot(),
1592 None,
1593 None,
1594 options.verbose,
1595 sys.stdout)
1596
1597 if masters:
1598 change_description = change_desc.description + '\nCQ_TRYBOTS='
1599 lst = []
1600 for master, mapping in masters.iteritems():
1601 lst.append(master + ':' + ','.join(mapping.keys()))
1602 change_desc.set_description(change_description + ';'.join(lst))
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001603 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001604 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001605
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001606 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607 print "Description is empty; aborting."
1608 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001609
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001610 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001611 if change_desc.get_reviewers():
1612 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001613 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001614 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001615 DieWithError("Must specify reviewers to send email.")
1616 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001617
1618 # We check this before applying rietveld.private assuming that in
1619 # rietveld.cc only addresses which we can send private CLs to are listed
1620 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1621 # --private is specified explicitly on the command line.
1622 if options.private:
1623 logging.warn('rietveld.cc is ignored since private flag is specified. '
1624 'You need to review and add them manually if necessary.')
1625 cc = cl.GetCCListWithoutDefault()
1626 else:
1627 cc = cl.GetCCList()
1628 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001629 if cc:
1630 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001631
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001632 if options.private or settings.GetDefaultPrivateFlag() == "True":
1633 upload_args.append('--private')
1634
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001635 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001636 if not options.find_copies:
1637 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001638
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 # Include the upstream repo's URL in the change -- this is useful for
1640 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001641 remote_url = cl.GetGitBaseUrlFromConfig()
1642 if not remote_url:
1643 if settings.GetIsGitSvn():
1644 # URL is dependent on the current directory.
1645 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1646 if data:
1647 keys = dict(line.split(': ', 1) for line in data.splitlines()
1648 if ': ' in line)
1649 remote_url = keys.get('URL', None)
1650 else:
1651 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1652 remote_url = (cl.GetRemoteUrl() + '@'
1653 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654 if remote_url:
1655 upload_args.extend(['--base_url', remote_url])
1656
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001657 project = settings.GetProject()
1658 if project:
1659 upload_args.extend(['--project', project])
1660
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001661 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001662 upload_args = ['upload'] + upload_args + args
1663 logging.info('upload.RealMain(%s)', upload_args)
1664 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001665 issue = int(issue)
1666 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001667 except KeyboardInterrupt:
1668 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001669 except:
1670 # If we got an exception after the user typed a description for their
1671 # change, back up the description before re-raising.
1672 if change_desc:
1673 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1674 print '\nGot exception while uploading -- saving description to %s\n' \
1675 % backup_path
1676 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001677 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001678 backup_file.close()
1679 raise
1680
1681 if not cl.GetIssue():
1682 cl.SetIssue(issue)
1683 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001684
1685 if options.use_commit_queue:
1686 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001687 return 0
1688
1689
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001690def cleanup_list(l):
1691 """Fixes a list so that comma separated items are put as individual items.
1692
1693 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1694 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1695 """
1696 items = sum((i.split(',') for i in l), [])
1697 stripped_items = (i.strip() for i in items)
1698 return sorted(filter(None, stripped_items))
1699
1700
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001701@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001702def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001703 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001704 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1705 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001706 parser.add_option('--bypass-watchlists', action='store_true',
1707 dest='bypass_watchlists',
1708 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001709 parser.add_option('-f', action='store_true', dest='force',
1710 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001711 parser.add_option('-m', dest='message', help='message for patchset')
1712 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001713 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001714 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001715 help='reviewer email addresses')
1716 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001717 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001718 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001719 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001720 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001721 parser.add_option('--emulate_svn_auto_props',
1722 '--emulate-svn-auto-props',
1723 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001724 dest="emulate_svn_auto_props",
1725 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001726 parser.add_option('-c', '--use-commit-queue', action='store_true',
1727 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001728 parser.add_option('--private', action='store_true',
1729 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001730 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001731 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001732 help='When uploading to gerrit, remote branch to '
1733 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001734 parser.add_option('--email', default=None,
1735 help='email address to use to connect to Rietveld')
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001736 parser.add_option('--auto-bots', default=False, action='store_true',
1737 help='Autogenerate which trybots to use for this CL')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001738
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001739 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001740 (options, args) = parser.parse_args(args)
1741
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001742 if options.target_branch and not settings.GetIsGerrit():
1743 parser.error('Use --target_branch for non gerrit repository.')
1744
ukai@chromium.org259e4682012-10-25 07:36:33 +00001745 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001746 return 1
1747
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001748 options.reviewers = cleanup_list(options.reviewers)
1749 options.cc = cleanup_list(options.cc)
1750
ukai@chromium.orge8077812012-02-03 03:41:46 +00001751 cl = Changelist()
1752 if args:
1753 # TODO(ukai): is it ok for gerrit case?
1754 base_branch = args[0]
1755 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001756 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001757 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001758 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001759
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001760 # Apply watchlists on upload.
1761 change = cl.GetChange(base_branch, None)
1762 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1763 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001764 if not options.bypass_watchlists:
1765 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001766
ukai@chromium.orge8077812012-02-03 03:41:46 +00001767 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001768 if options.reviewers:
1769 # Set the reviewer list now so that presubmit checks can access it.
1770 change_description = ChangeDescription(change.FullDescriptionText())
1771 change_description.update_reviewers(options.reviewers)
1772 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001773 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001774 may_prompt=not options.force,
1775 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001776 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001777 if not hook_results.should_continue():
1778 return 1
1779 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001780 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001781
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001782 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001783 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001784 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001785 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001786 print ('The last upload made from this repository was patchset #%d but '
1787 'the most recent patchset on the server is #%d.'
1788 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001789 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1790 'from another machine or branch the patch you\'re uploading now '
1791 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001792 ask_for_data('About to upload; enter to confirm.')
1793
iannucci@chromium.org79540052012-10-19 23:15:26 +00001794 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001795 if settings.GetIsGerrit():
1796 return GerritUpload(options, args, cl)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001797 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001798 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001799 git_set_branch_value('last-upload-hash',
1800 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001801
1802 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001803
1804
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001805def IsSubmoduleMergeCommit(ref):
1806 # When submodules are added to the repo, we expect there to be a single
1807 # non-git-svn merge commit at remote HEAD with a signature comment.
1808 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001809 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001810 return RunGit(cmd) != ''
1811
1812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001813def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001814 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001816 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817 Updates changelog with metadata (e.g. pointer to review).
1818 Pushes/dcommits the code upstream.
1819 Updates review and closes.
1820 """
1821 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1822 help='bypass upload presubmit hook')
1823 parser.add_option('-m', dest='message',
1824 help="override review description")
1825 parser.add_option('-f', action='store_true', dest='force',
1826 help="force yes to questions (don't prompt)")
1827 parser.add_option('-c', dest='contributor',
1828 help="external contributor for patch (appended to " +
1829 "description and used as author for git). Should be " +
1830 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001831 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832 (options, args) = parser.parse_args(args)
1833 cl = Changelist()
1834
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001835 current = cl.GetBranch()
1836 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1837 if not settings.GetIsGitSvn() and remote == '.':
1838 print
1839 print 'Attempting to push branch %r into another local branch!' % current
1840 print
1841 print 'Either reparent this branch on top of origin/master:'
1842 print ' git reparent-branch --root'
1843 print
1844 print 'OR run `git rebase-update` if you think the parent branch is already'
1845 print 'committed.'
1846 print
1847 print ' Current parent: %r' % upstream_branch
1848 return 1
1849
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001850 if not args or cmd == 'push':
1851 # Default to merging against our best guess of the upstream branch.
1852 args = [cl.GetUpstreamBranch()]
1853
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001854 if options.contributor:
1855 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1856 print "Please provide contibutor as 'First Last <email@example.com>'"
1857 return 1
1858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001860 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861
ukai@chromium.org259e4682012-10-25 07:36:33 +00001862 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 return 1
1864
1865 # This rev-list syntax means "show all commits not in my branch that
1866 # are in base_branch".
1867 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1868 base_branch]).splitlines()
1869 if upstream_commits:
1870 print ('Base branch "%s" has %d commits '
1871 'not in this branch.' % (base_branch, len(upstream_commits)))
1872 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1873 return 1
1874
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001875 # This is the revision `svn dcommit` will commit on top of.
1876 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1877 '--pretty=format:%H'])
1878
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001879 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001880 # If the base_head is a submodule merge commit, the first parent of the
1881 # base_head should be a git-svn commit, which is what we're interested in.
1882 base_svn_head = base_branch
1883 if base_has_submodules:
1884 base_svn_head += '^1'
1885
1886 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887 if extra_commits:
1888 print ('This branch has %d additional commits not upstreamed yet.'
1889 % len(extra_commits.splitlines()))
1890 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1891 'before attempting to %s.' % (base_branch, cmd))
1892 return 1
1893
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001894 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001895 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001896 author = None
1897 if options.contributor:
1898 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001899 hook_results = cl.RunHook(
1900 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001901 may_prompt=not options.force,
1902 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001903 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001904 if not hook_results.should_continue():
1905 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906
1907 if cmd == 'dcommit':
1908 # Check the tree status if the tree status URL is set.
1909 status = GetTreeStatus()
1910 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001911 print('The tree is closed. Please wait for it to reopen. Use '
1912 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 return 1
1914 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001915 print('Unable to determine tree status. Please verify manually and '
1916 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001917 else:
1918 breakpad.SendStack(
1919 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001920 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1921 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001922 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001924 change_desc = ChangeDescription(options.message)
1925 if not change_desc.description and cl.GetIssue():
1926 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001928 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001929 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001930 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001931 else:
1932 print 'No description set.'
1933 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1934 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001935
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001936 # Keep a separate copy for the commit message, because the commit message
1937 # contains the link to the Rietveld issue, while the Rietveld message contains
1938 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001939 # Keep a separate copy for the commit message.
1940 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001941 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001942
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001943 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001944 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001945 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001947 commit_desc.append_footer('Patch from %s.' % options.contributor)
1948
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001949 print('Description:')
1950 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951
1952 branches = [base_branch, cl.GetBranchRef()]
1953 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001954 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001956 # We want to squash all this branch's commits into one commit with the proper
1957 # description. We do this by doing a "reset --soft" to the base branch (which
1958 # keeps the working copy the same), then dcommitting that. If origin/master
1959 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1960 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001961 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001962 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1963 # Delete the branches if they exist.
1964 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1965 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1966 result = RunGitWithCode(showref_cmd)
1967 if result[0] == 0:
1968 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001969
1970 # We might be in a directory that's present in this branch but not in the
1971 # trunk. Move up to the top of the tree so that git commands that expect a
1972 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001973 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 if rel_base_path:
1975 os.chdir(rel_base_path)
1976
1977 # Stuff our change into the merge branch.
1978 # We wrap in a try...finally block so if anything goes wrong,
1979 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001980 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001982 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1983 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001984 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001985 RunGit(
1986 [
1987 'commit', '--author', options.contributor,
1988 '-m', commit_desc.description,
1989 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001991 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001992 if base_has_submodules:
1993 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1994 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1995 RunGit(['checkout', CHERRY_PICK_BRANCH])
1996 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997 if cmd == 'push':
1998 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001999 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000 retcode, output = RunGitWithCode(
2001 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
2002 logging.debug(output)
2003 else:
2004 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002005 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002006 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002007 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002008 finally:
2009 # And then swap back to the original branch and clean up.
2010 RunGit(['checkout', '-q', cl.GetBranch()])
2011 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002012 if base_has_submodules:
2013 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002014
2015 if cl.GetIssue():
2016 if cmd == 'dcommit' and 'Committed r' in output:
2017 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2018 elif cmd == 'push' and retcode == 0:
mark@chromium.org671c7a32014-07-31 17:09:36 +00002019 match = (re.match(r'.*?([a-f0-9]{7,})\.\.([a-f0-9]{7,})$', l)
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00002020 for l in output.splitlines(False))
2021 match = filter(None, match)
2022 if len(match) != 1:
2023 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
2024 output)
2025 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 else:
2027 return 1
2028 viewvc_url = settings.GetViewVCUrl()
2029 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002030 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00002031 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002032 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033 print ('Closing issue '
2034 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002035 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002037 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002038 patch_num = len(props['patchsets'])
rsesek@chromium.org27b15a32014-08-01 01:06:24 +00002039 comment = "Committed patchset #%d manually as %s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002040 if options.bypass_hooks:
2041 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2042 else:
2043 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002044 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002045 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002046
2047 if retcode == 0:
2048 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2049 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002050 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002051
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002052 return 0
2053
2054
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002055@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002057 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002059 message = """This doesn't appear to be an SVN repository.
2060If your project has a git mirror with an upstream SVN master, you probably need
2061to run 'git svn init', see your project's git mirror documentation.
2062If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002063to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002064Choose wisely, if you get this wrong, your commit might appear to succeed but
2065will instead be silently ignored."""
2066 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002067 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 return SendUpstream(parser, args, 'dcommit')
2069
2070
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002071@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002072def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002073 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002074 if settings.GetIsGitSvn():
2075 print('This appears to be an SVN repository.')
2076 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002077 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002078 return SendUpstream(parser, args, 'push')
2079
2080
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002081@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002082def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002083 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084 parser.add_option('-b', dest='newbranch',
2085 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002086 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002087 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002088 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2089 help='Change to the directory DIR immediately, '
2090 'before doing anything else.')
2091 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002092 help='failed patches spew .rej files rather than '
2093 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002094 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2095 help="don't commit after patch applies")
2096 (options, args) = parser.parse_args(args)
2097 if len(args) != 1:
2098 parser.print_help()
2099 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002100 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002102 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002103 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002104
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002105 if options.newbranch:
2106 if options.force:
2107 RunGit(['branch', '-D', options.newbranch],
2108 stderr=subprocess2.PIPE, error_ok=True)
2109 RunGit(['checkout', '-b', options.newbranch,
2110 Changelist().GetUpstreamBranch()])
2111
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002112 return PatchIssue(issue_arg, options.reject, options.nocommit,
2113 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002114
2115
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002116def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002117 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002119 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002120 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002121 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002122 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002124 # Assume it's a URL to the patch. Default to https.
2125 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002126 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002127 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002128 DieWithError('Must pass an issue ID or full URL for '
2129 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002130 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002131 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002132 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002134 # Switch up to the top-level directory, if necessary, in preparation for
2135 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002136 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002137 if top:
2138 os.chdir(top)
2139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002140 # Git patches have a/ at the beginning of source paths. We strip that out
2141 # with a sed script rather than the -p flag to patch so we can feed either
2142 # Git or svn-style patches into the same apply command.
2143 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002144 try:
2145 patch_data = subprocess2.check_output(
2146 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2147 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 DieWithError('Git patch mungling failed.')
2149 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002150
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151 # We use "git apply" to apply the patch instead of "patch" so that we can
2152 # pick up file adds.
2153 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002154 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002155 if directory:
2156 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002157 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002159 elif IsGitVersionAtLeast('1.7.12'):
2160 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002161 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002162 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002163 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002164 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165 DieWithError('Failed to apply the patch')
2166
2167 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002168 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002169 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2170 cl = Changelist()
2171 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002172 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002173 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002174 else:
2175 print "Patch applied to index."
2176 return 0
2177
2178
2179def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002180 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002181 # Provide a wrapper for git svn rebase to help avoid accidental
2182 # git svn dcommit.
2183 # It's the only command that doesn't use parser at all since we just defer
2184 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002185
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002186 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187
2188
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002189def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190 """Fetches the tree status and returns either 'open', 'closed',
2191 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002192 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002193 if url:
2194 status = urllib2.urlopen(url).read().lower()
2195 if status.find('closed') != -1 or status == '0':
2196 return 'closed'
2197 elif status.find('open') != -1 or status == '1':
2198 return 'open'
2199 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200 return 'unset'
2201
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002202
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203def GetTreeStatusReason():
2204 """Fetches the tree status from a json url and returns the message
2205 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002206 url = settings.GetTreeStatusUrl()
2207 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002208 connection = urllib2.urlopen(json_url)
2209 status = json.loads(connection.read())
2210 connection.close()
2211 return status['message']
2212
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002213
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002214def GetBuilderMaster(bot_list):
2215 """For a given builder, fetch the master from AE if available."""
2216 map_url = 'https://builders-map.appspot.com/'
2217 try:
2218 master_map = json.load(urllib2.urlopen(map_url))
2219 except urllib2.URLError as e:
2220 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2221 (map_url, e))
2222 except ValueError as e:
2223 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2224 if not master_map:
2225 return None, 'Failed to build master map.'
2226
2227 result_master = ''
2228 for bot in bot_list:
2229 builder = bot.split(':', 1)[0]
2230 master_list = master_map.get(builder, [])
2231 if not master_list:
2232 return None, ('No matching master for builder %s.' % builder)
2233 elif len(master_list) > 1:
2234 return None, ('The builder name %s exists in multiple masters %s.' %
2235 (builder, master_list))
2236 else:
2237 cur_master = master_list[0]
2238 if not result_master:
2239 result_master = cur_master
2240 elif result_master != cur_master:
2241 return None, 'The builders do not belong to the same master.'
2242 return result_master, None
2243
2244
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002245def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002246 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002247 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002248 status = GetTreeStatus()
2249 if 'unset' == status:
2250 print 'You must configure your tree status URL by running "git cl config".'
2251 return 2
2252
2253 print "The tree is %s" % status
2254 print
2255 print GetTreeStatusReason()
2256 if status != 'open':
2257 return 1
2258 return 0
2259
2260
maruel@chromium.org15192402012-09-06 12:38:29 +00002261def CMDtry(parser, args):
2262 """Triggers a try job through Rietveld."""
2263 group = optparse.OptionGroup(parser, "Try job options")
2264 group.add_option(
2265 "-b", "--bot", action="append",
2266 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2267 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002268 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002269 "the try server waterfall for the builders name and the tests "
2270 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002271 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002272 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002273 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002274 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002275 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002276 "-r", "--revision",
2277 help="Revision to use for the try job; default: the "
2278 "revision will be determined by the try server; see "
2279 "its waterfall for more info")
2280 group.add_option(
2281 "-c", "--clobber", action="store_true", default=False,
2282 help="Force a clobber before building; e.g. don't do an "
2283 "incremental build")
2284 group.add_option(
2285 "--project",
2286 help="Override which project to use. Projects are defined "
2287 "server-side to define what default bot set to use")
2288 group.add_option(
2289 "-t", "--testfilter", action="append", default=[],
2290 help=("Apply a testfilter to all the selected builders. Unless the "
2291 "builders configurations are similar, use multiple "
2292 "--bot <builder>:<test> arguments."))
2293 group.add_option(
2294 "-n", "--name", help="Try job name; default to current branch name")
2295 parser.add_option_group(group)
2296 options, args = parser.parse_args(args)
2297
2298 if args:
2299 parser.error('Unknown arguments: %s' % args)
2300
2301 cl = Changelist()
2302 if not cl.GetIssue():
2303 parser.error('Need to upload first')
2304
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002305 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002306 if props.get('closed'):
2307 parser.error('Cannot send tryjobs for a closed CL')
2308
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002309 if props.get('private'):
2310 parser.error('Cannot use trybots with private issue')
2311
maruel@chromium.org15192402012-09-06 12:38:29 +00002312 if not options.name:
2313 options.name = cl.GetBranch()
2314
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002315 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002316 options.master, err_msg = GetBuilderMaster(options.bot)
2317 if err_msg:
2318 parser.error('Tryserver master cannot be found because: %s\n'
2319 'Please manually specify the tryserver master'
2320 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002321
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002322 def GetMasterMap():
2323 # Process --bot and --testfilter.
2324 if not options.bot:
2325 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002326
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002327 # Get try masters from PRESUBMIT.py files.
2328 masters = presubmit_support.DoGetTryMasters(
2329 change,
2330 change.LocalPaths(),
2331 settings.GetRoot(),
2332 None,
2333 None,
2334 options.verbose,
2335 sys.stdout)
2336 if masters:
2337 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002338
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002339 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2340 options.bot = presubmit_support.DoGetTrySlaves(
2341 change,
2342 change.LocalPaths(),
2343 settings.GetRoot(),
2344 None,
2345 None,
2346 options.verbose,
2347 sys.stdout)
2348 if not options.bot:
2349 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002350
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002351 builders_and_tests = {}
2352 # TODO(machenbach): The old style command-line options don't support
2353 # multiple try masters yet.
2354 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2355 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2356
2357 for bot in old_style:
2358 if ':' in bot:
2359 builder, tests = bot.split(':', 1)
2360 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2361 elif ',' in bot:
2362 parser.error('Specify one bot per --bot flag')
2363 else:
2364 builders_and_tests.setdefault(bot, []).append('defaulttests')
2365
2366 for bot, tests in new_style:
2367 builders_and_tests.setdefault(bot, []).extend(tests)
2368
2369 # Return a master map with one master to be backwards compatible. The
2370 # master name defaults to an empty string, which will cause the master
2371 # not to be set on rietveld (deprecated).
2372 return {options.master: builders_and_tests}
2373
2374 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002375
maruel@chromium.org15192402012-09-06 12:38:29 +00002376 if options.testfilter:
2377 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002378 masters = dict((master, dict(
2379 (b, forced_tests) for b, t in slaves.iteritems()
2380 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002381
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002382 for builders in masters.itervalues():
2383 if any('triggered' in b for b in builders):
2384 print >> sys.stderr, (
2385 'ERROR You are trying to send a job to a triggered bot. This type of'
2386 ' bot requires an\ninitial job from a parent (usually a builder). '
2387 'Instead send your job to the parent.\n'
2388 'Bot list: %s' % builders)
2389 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002390
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002391 patchset = cl.GetMostRecentPatchset()
2392 if patchset and patchset != cl.GetPatchset():
2393 print(
2394 '\nWARNING Mismatch between local config and server. Did a previous '
2395 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2396 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002397 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002398 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002399 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002400 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002401 except urllib2.HTTPError, e:
2402 if e.code == 404:
2403 print('404 from rietveld; '
2404 'did you mean to use "git try" instead of "git cl try"?')
2405 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002406 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002407
2408 for (master, builders) in masters.iteritems():
2409 if master:
2410 print 'Master: %s' % master
2411 length = max(len(builder) for builder in builders)
2412 for builder in sorted(builders):
2413 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002414 return 0
2415
2416
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002417@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002418def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002419 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002420 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002421 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002422 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002423
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002424 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002425 if args:
2426 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002427 branch = cl.GetBranch()
2428 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002429 cl = Changelist()
2430 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002431
2432 # Clear configured merge-base, if there is one.
2433 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002434 else:
2435 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436 return 0
2437
2438
thestig@chromium.org00858c82013-12-02 23:08:03 +00002439def CMDweb(parser, args):
2440 """Opens the current CL in the web browser."""
2441 _, args = parser.parse_args(args)
2442 if args:
2443 parser.error('Unrecognized args: %s' % ' '.join(args))
2444
2445 issue_url = Changelist().GetIssueURL()
2446 if not issue_url:
2447 print >> sys.stderr, 'ERROR No issue to open'
2448 return 1
2449
2450 webbrowser.open(issue_url)
2451 return 0
2452
2453
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002454def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002455 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002456 _, args = parser.parse_args(args)
2457 if args:
2458 parser.error('Unrecognized args: %s' % ' '.join(args))
2459 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002460 props = cl.GetIssueProperties()
2461 if props.get('private'):
2462 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002463 cl.SetFlag('commit', '1')
2464 return 0
2465
2466
groby@chromium.org411034a2013-02-26 15:12:01 +00002467def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002468 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002469 _, args = parser.parse_args(args)
2470 if args:
2471 parser.error('Unrecognized args: %s' % ' '.join(args))
2472 cl = Changelist()
2473 # Ensure there actually is an issue to close.
2474 cl.GetDescription()
2475 cl.CloseIssue()
2476 return 0
2477
2478
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002479def CMDdiff(parser, args):
2480 """shows differences between local tree and last upload."""
2481 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002482 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002483 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002484 if not issue:
2485 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002486 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002487 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002488
2489 # Create a new branch based on the merge-base
2490 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2491 try:
2492 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002493 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002494 if rtn != 0:
2495 return rtn
2496
2497 # Switch back to starting brand and diff against the temporary
2498 # branch containing the latest rietveld patch.
2499 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2500 finally:
2501 RunGit(['checkout', '-q', branch])
2502 RunGit(['branch', '-D', TMP_BRANCH])
2503
2504 return 0
2505
2506
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002507def CMDowners(parser, args):
2508 """interactively find the owners for reviewing"""
2509 parser.add_option(
2510 '--no-color',
2511 action='store_true',
2512 help='Use this option to disable color output')
2513 options, args = parser.parse_args(args)
2514
2515 author = RunGit(['config', 'user.email']).strip() or None
2516
2517 cl = Changelist()
2518
2519 if args:
2520 if len(args) > 1:
2521 parser.error('Unknown args')
2522 base_branch = args[0]
2523 else:
2524 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002525 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002526
2527 change = cl.GetChange(base_branch, None)
2528 return owners_finder.OwnersFinder(
2529 [f.LocalPath() for f in
2530 cl.GetChange(base_branch, None).AffectedFiles()],
2531 change.RepositoryRoot(), author,
2532 fopen=file, os_path=os.path, glob=glob.glob,
2533 disable_color=options.no_color).run()
2534
2535
enne@chromium.org555cfe42014-01-29 18:21:39 +00002536@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002537def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002538 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002539 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002540 parser.add_option('--full', action='store_true',
2541 help='Reformat the full content of all touched files')
2542 parser.add_option('--dry-run', action='store_true',
2543 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002544 parser.add_option('--diff', action='store_true',
2545 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002546 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002547
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002548 # git diff generates paths against the root of the repository. Change
2549 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002550 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002551 if rel_base_path:
2552 os.chdir(rel_base_path)
2553
digit@chromium.org29e47272013-05-17 17:01:46 +00002554 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002555 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002556 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002557 # Only list the names of modified files.
2558 diff_cmd.append('--name-only')
2559 else:
2560 # Only generate context-less patches.
2561 diff_cmd.append('-U0')
2562
2563 # Grab the merge-base commit, i.e. the upstream commit of the current
2564 # branch when it was created or the last time it was rebased. This is
2565 # to cover the case where the user may have called "git fetch origin",
2566 # moving the origin branch to a newer commit, but hasn't rebased yet.
2567 upstream_commit = None
2568 cl = Changelist()
2569 upstream_branch = cl.GetUpstreamBranch()
2570 if upstream_branch:
2571 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2572 upstream_commit = upstream_commit.strip()
2573
2574 if not upstream_commit:
2575 DieWithError('Could not find base commit for this branch. '
2576 'Are you in detached state?')
2577
2578 diff_cmd.append(upstream_commit)
2579
2580 # Handle source file filtering.
2581 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002582 if args:
2583 for arg in args:
2584 if os.path.isdir(arg):
2585 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2586 elif os.path.isfile(arg):
2587 diff_cmd.append(arg)
2588 else:
2589 DieWithError('Argument "%s" is not a file or a directory' % arg)
2590 else:
2591 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002592 diff_output = RunGit(diff_cmd)
2593
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002594 top_dir = os.path.normpath(
2595 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2596
2597 # Locate the clang-format binary in the checkout
2598 try:
2599 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2600 except clang_format.NotFoundError, e:
2601 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002602
digit@chromium.org29e47272013-05-17 17:01:46 +00002603 if opts.full:
2604 # diff_output is a list of files to send to clang-format.
2605 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002606 if not files:
2607 print "Nothing to format."
2608 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002609 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002610 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002611 cmd.append('-i')
2612 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002613 if opts.diff:
2614 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002615 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002616 env = os.environ.copy()
2617 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002618 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002619 try:
2620 script = clang_format.FindClangFormatScriptInChromiumTree(
2621 'clang-format-diff.py')
2622 except clang_format.NotFoundError, e:
2623 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002624
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002625 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002626 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002627 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002628
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002629 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002630 if opts.diff:
2631 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002632 if opts.dry_run and len(stdout) > 0:
2633 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002634
2635 return 0
2636
2637
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002638class OptionParser(optparse.OptionParser):
2639 """Creates the option parse and add --verbose support."""
2640 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002641 optparse.OptionParser.__init__(
2642 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002643 self.add_option(
2644 '-v', '--verbose', action='count', default=0,
2645 help='Use 2 times for more debugging info')
2646
2647 def parse_args(self, args=None, values=None):
2648 options, args = optparse.OptionParser.parse_args(self, args, values)
2649 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2650 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2651 return options, args
2652
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002654def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002655 if sys.hexversion < 0x02060000:
2656 print >> sys.stderr, (
2657 '\nYour python version %s is unsupported, please upgrade.\n' %
2658 sys.version.split(' ', 1)[0])
2659 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002660
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002661 # Reload settings.
2662 global settings
2663 settings = Settings()
2664
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002665 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002666 dispatcher = subcommand.CommandDispatcher(__name__)
2667 try:
2668 return dispatcher.execute(OptionParser(), argv)
2669 except urllib2.HTTPError, e:
2670 if e.code != 500:
2671 raise
2672 DieWithError(
2673 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2674 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002675
2676
2677if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002678 # These affect sys.stdout so do it outside of main() to simplify mocks in
2679 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002680 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002681 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002682 sys.exit(main(sys.argv[1:]))