blob: 77f4a9e9f13c769e02e6acdadb087acb64f05224 [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')
1721 parser.add_option("--emulate_svn_auto_props", action="store_true",
1722 dest="emulate_svn_auto_props",
1723 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001724 parser.add_option('-c', '--use-commit-queue', action='store_true',
1725 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001726 parser.add_option('--private', action='store_true',
1727 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001728 parser.add_option('--target_branch',
1729 help='When uploading to gerrit, remote branch to '
1730 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001731 parser.add_option('--email', default=None,
1732 help='email address to use to connect to Rietveld')
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001733 parser.add_option('--auto-bots', default=False, action='store_true',
1734 help='Autogenerate which trybots to use for this CL')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001735
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001736 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001737 (options, args) = parser.parse_args(args)
1738
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001739 if options.target_branch and not settings.GetIsGerrit():
1740 parser.error('Use --target_branch for non gerrit repository.')
1741
ukai@chromium.org259e4682012-10-25 07:36:33 +00001742 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001743 return 1
1744
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001745 options.reviewers = cleanup_list(options.reviewers)
1746 options.cc = cleanup_list(options.cc)
1747
ukai@chromium.orge8077812012-02-03 03:41:46 +00001748 cl = Changelist()
1749 if args:
1750 # TODO(ukai): is it ok for gerrit case?
1751 base_branch = args[0]
1752 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001753 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001754 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001755 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001756
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001757 # Apply watchlists on upload.
1758 change = cl.GetChange(base_branch, None)
1759 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1760 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001761 if not options.bypass_watchlists:
1762 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001763
ukai@chromium.orge8077812012-02-03 03:41:46 +00001764 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001765 if options.reviewers:
1766 # Set the reviewer list now so that presubmit checks can access it.
1767 change_description = ChangeDescription(change.FullDescriptionText())
1768 change_description.update_reviewers(options.reviewers)
1769 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001770 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001771 may_prompt=not options.force,
1772 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001773 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001774 if not hook_results.should_continue():
1775 return 1
1776 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001777 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001778
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001779 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001780 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001781 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001782 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001783 print ('The last upload made from this repository was patchset #%d but '
1784 'the most recent patchset on the server is #%d.'
1785 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001786 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1787 'from another machine or branch the patch you\'re uploading now '
1788 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001789 ask_for_data('About to upload; enter to confirm.')
1790
iannucci@chromium.org79540052012-10-19 23:15:26 +00001791 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001792 if settings.GetIsGerrit():
1793 return GerritUpload(options, args, cl)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001794 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001795 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001796 git_set_branch_value('last-upload-hash',
1797 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001798
1799 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001800
1801
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001802def IsSubmoduleMergeCommit(ref):
1803 # When submodules are added to the repo, we expect there to be a single
1804 # non-git-svn merge commit at remote HEAD with a signature comment.
1805 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001806 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001807 return RunGit(cmd) != ''
1808
1809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001811 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001813 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814 Updates changelog with metadata (e.g. pointer to review).
1815 Pushes/dcommits the code upstream.
1816 Updates review and closes.
1817 """
1818 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1819 help='bypass upload presubmit hook')
1820 parser.add_option('-m', dest='message',
1821 help="override review description")
1822 parser.add_option('-f', action='store_true', dest='force',
1823 help="force yes to questions (don't prompt)")
1824 parser.add_option('-c', dest='contributor',
1825 help="external contributor for patch (appended to " +
1826 "description and used as author for git). Should be " +
1827 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001828 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 (options, args) = parser.parse_args(args)
1830 cl = Changelist()
1831
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001832 current = cl.GetBranch()
1833 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1834 if not settings.GetIsGitSvn() and remote == '.':
1835 print
1836 print 'Attempting to push branch %r into another local branch!' % current
1837 print
1838 print 'Either reparent this branch on top of origin/master:'
1839 print ' git reparent-branch --root'
1840 print
1841 print 'OR run `git rebase-update` if you think the parent branch is already'
1842 print 'committed.'
1843 print
1844 print ' Current parent: %r' % upstream_branch
1845 return 1
1846
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847 if not args or cmd == 'push':
1848 # Default to merging against our best guess of the upstream branch.
1849 args = [cl.GetUpstreamBranch()]
1850
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001851 if options.contributor:
1852 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1853 print "Please provide contibutor as 'First Last <email@example.com>'"
1854 return 1
1855
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001856 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001857 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001858
ukai@chromium.org259e4682012-10-25 07:36:33 +00001859 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 return 1
1861
1862 # This rev-list syntax means "show all commits not in my branch that
1863 # are in base_branch".
1864 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1865 base_branch]).splitlines()
1866 if upstream_commits:
1867 print ('Base branch "%s" has %d commits '
1868 'not in this branch.' % (base_branch, len(upstream_commits)))
1869 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1870 return 1
1871
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001872 # This is the revision `svn dcommit` will commit on top of.
1873 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1874 '--pretty=format:%H'])
1875
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001877 # If the base_head is a submodule merge commit, the first parent of the
1878 # base_head should be a git-svn commit, which is what we're interested in.
1879 base_svn_head = base_branch
1880 if base_has_submodules:
1881 base_svn_head += '^1'
1882
1883 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884 if extra_commits:
1885 print ('This branch has %d additional commits not upstreamed yet.'
1886 % len(extra_commits.splitlines()))
1887 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1888 'before attempting to %s.' % (base_branch, cmd))
1889 return 1
1890
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001891 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001892 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001893 author = None
1894 if options.contributor:
1895 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001896 hook_results = cl.RunHook(
1897 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001898 may_prompt=not options.force,
1899 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001900 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001901 if not hook_results.should_continue():
1902 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903
1904 if cmd == 'dcommit':
1905 # Check the tree status if the tree status URL is set.
1906 status = GetTreeStatus()
1907 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001908 print('The tree is closed. Please wait for it to reopen. Use '
1909 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 return 1
1911 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001912 print('Unable to determine tree status. Please verify manually and '
1913 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001914 else:
1915 breakpad.SendStack(
1916 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001917 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1918 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001919 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001921 change_desc = ChangeDescription(options.message)
1922 if not change_desc.description and cl.GetIssue():
1923 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001925 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001926 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001927 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001928 else:
1929 print 'No description set.'
1930 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1931 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001933 # Keep a separate copy for the commit message, because the commit message
1934 # contains the link to the Rietveld issue, while the Rietveld message contains
1935 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001936 # Keep a separate copy for the commit message.
1937 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001938 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001939
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001940 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001941 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001942 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001944 commit_desc.append_footer('Patch from %s.' % options.contributor)
1945
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001946 print('Description:')
1947 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948
1949 branches = [base_branch, cl.GetBranchRef()]
1950 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001951 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001953 # We want to squash all this branch's commits into one commit with the proper
1954 # description. We do this by doing a "reset --soft" to the base branch (which
1955 # keeps the working copy the same), then dcommitting that. If origin/master
1956 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1957 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001959 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1960 # Delete the branches if they exist.
1961 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1962 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1963 result = RunGitWithCode(showref_cmd)
1964 if result[0] == 0:
1965 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966
1967 # We might be in a directory that's present in this branch but not in the
1968 # trunk. Move up to the top of the tree so that git commands that expect a
1969 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001970 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971 if rel_base_path:
1972 os.chdir(rel_base_path)
1973
1974 # Stuff our change into the merge branch.
1975 # We wrap in a try...finally block so if anything goes wrong,
1976 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001977 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001978 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001979 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1980 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001982 RunGit(
1983 [
1984 'commit', '--author', options.contributor,
1985 '-m', commit_desc.description,
1986 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001988 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001989 if base_has_submodules:
1990 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1991 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1992 RunGit(['checkout', CHERRY_PICK_BRANCH])
1993 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994 if cmd == 'push':
1995 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001996 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997 retcode, output = RunGitWithCode(
1998 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1999 logging.debug(output)
2000 else:
2001 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002002 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002003 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002004 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002005 finally:
2006 # And then swap back to the original branch and clean up.
2007 RunGit(['checkout', '-q', cl.GetBranch()])
2008 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002009 if base_has_submodules:
2010 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011
2012 if cl.GetIssue():
2013 if cmd == 'dcommit' and 'Committed r' in output:
2014 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2015 elif cmd == 'push' and retcode == 0:
mark@chromium.org671c7a32014-07-31 17:09:36 +00002016 match = (re.match(r'.*?([a-f0-9]{7,})\.\.([a-f0-9]{7,})$', l)
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00002017 for l in output.splitlines(False))
2018 match = filter(None, match)
2019 if len(match) != 1:
2020 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
2021 output)
2022 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 else:
2024 return 1
2025 viewvc_url = settings.GetViewVCUrl()
2026 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002027 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00002028 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002029 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030 print ('Closing issue '
2031 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002032 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002034 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002035 patch_num = len(props['patchsets'])
rsesek@chromium.org27b15a32014-08-01 01:06:24 +00002036 comment = "Committed patchset #%d manually as %s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002037 if options.bypass_hooks:
2038 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2039 else:
2040 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002041 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002042 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002043
2044 if retcode == 0:
2045 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2046 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002047 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049 return 0
2050
2051
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002052@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002054 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002056 message = """This doesn't appear to be an SVN repository.
2057If your project has a git mirror with an upstream SVN master, you probably need
2058to run 'git svn init', see your project's git mirror documentation.
2059If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002060to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002061Choose wisely, if you get this wrong, your commit might appear to succeed but
2062will instead be silently ignored."""
2063 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002064 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 return SendUpstream(parser, args, 'dcommit')
2066
2067
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002068@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002069def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002070 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071 if settings.GetIsGitSvn():
2072 print('This appears to be an SVN repository.')
2073 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002074 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075 return SendUpstream(parser, args, 'push')
2076
2077
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002078@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002080 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002081 parser.add_option('-b', dest='newbranch',
2082 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002083 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002085 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2086 help='Change to the directory DIR immediately, '
2087 'before doing anything else.')
2088 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002089 help='failed patches spew .rej files rather than '
2090 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2092 help="don't commit after patch applies")
2093 (options, args) = parser.parse_args(args)
2094 if len(args) != 1:
2095 parser.print_help()
2096 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002097 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002099 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002100 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002101
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002102 if options.newbranch:
2103 if options.force:
2104 RunGit(['branch', '-D', options.newbranch],
2105 stderr=subprocess2.PIPE, error_ok=True)
2106 RunGit(['checkout', '-b', options.newbranch,
2107 Changelist().GetUpstreamBranch()])
2108
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002109 return PatchIssue(issue_arg, options.reject, options.nocommit,
2110 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002111
2112
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002113def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002114 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002116 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002117 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002118 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002119 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002121 # Assume it's a URL to the patch. Default to https.
2122 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002123 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002124 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 DieWithError('Must pass an issue ID or full URL for '
2126 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002127 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002128 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002129 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002131 # Switch up to the top-level directory, if necessary, in preparation for
2132 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002133 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002134 if top:
2135 os.chdir(top)
2136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002137 # Git patches have a/ at the beginning of source paths. We strip that out
2138 # with a sed script rather than the -p flag to patch so we can feed either
2139 # Git or svn-style patches into the same apply command.
2140 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002141 try:
2142 patch_data = subprocess2.check_output(
2143 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2144 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002145 DieWithError('Git patch mungling failed.')
2146 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 # We use "git apply" to apply the patch instead of "patch" so that we can
2149 # pick up file adds.
2150 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002151 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002152 if directory:
2153 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002154 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002155 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002156 elif IsGitVersionAtLeast('1.7.12'):
2157 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002158 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002159 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002160 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002161 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162 DieWithError('Failed to apply the patch')
2163
2164 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002165 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002166 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2167 cl = Changelist()
2168 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002169 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002170 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002171 else:
2172 print "Patch applied to index."
2173 return 0
2174
2175
2176def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002177 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002178 # Provide a wrapper for git svn rebase to help avoid accidental
2179 # git svn dcommit.
2180 # It's the only command that doesn't use parser at all since we just defer
2181 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002182
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002183 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002184
2185
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002186def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187 """Fetches the tree status and returns either 'open', 'closed',
2188 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002189 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190 if url:
2191 status = urllib2.urlopen(url).read().lower()
2192 if status.find('closed') != -1 or status == '0':
2193 return 'closed'
2194 elif status.find('open') != -1 or status == '1':
2195 return 'open'
2196 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197 return 'unset'
2198
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002199
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200def GetTreeStatusReason():
2201 """Fetches the tree status from a json url and returns the message
2202 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002203 url = settings.GetTreeStatusUrl()
2204 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002205 connection = urllib2.urlopen(json_url)
2206 status = json.loads(connection.read())
2207 connection.close()
2208 return status['message']
2209
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002210
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002211def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002212 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002213 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002214 status = GetTreeStatus()
2215 if 'unset' == status:
2216 print 'You must configure your tree status URL by running "git cl config".'
2217 return 2
2218
2219 print "The tree is %s" % status
2220 print
2221 print GetTreeStatusReason()
2222 if status != 'open':
2223 return 1
2224 return 0
2225
2226
maruel@chromium.org15192402012-09-06 12:38:29 +00002227def CMDtry(parser, args):
2228 """Triggers a try job through Rietveld."""
2229 group = optparse.OptionGroup(parser, "Try job options")
2230 group.add_option(
2231 "-b", "--bot", action="append",
2232 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2233 "times to specify multiple builders. ex: "
2234 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2235 "the try server waterfall for the builders name and the tests "
2236 "available. Can also be used to specify gtest_filter, e.g. "
2237 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2238 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002239 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002240 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002241 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002242 "-r", "--revision",
2243 help="Revision to use for the try job; default: the "
2244 "revision will be determined by the try server; see "
2245 "its waterfall for more info")
2246 group.add_option(
2247 "-c", "--clobber", action="store_true", default=False,
2248 help="Force a clobber before building; e.g. don't do an "
2249 "incremental build")
2250 group.add_option(
2251 "--project",
2252 help="Override which project to use. Projects are defined "
2253 "server-side to define what default bot set to use")
2254 group.add_option(
2255 "-t", "--testfilter", action="append", default=[],
2256 help=("Apply a testfilter to all the selected builders. Unless the "
2257 "builders configurations are similar, use multiple "
2258 "--bot <builder>:<test> arguments."))
2259 group.add_option(
2260 "-n", "--name", help="Try job name; default to current branch name")
2261 parser.add_option_group(group)
2262 options, args = parser.parse_args(args)
2263
2264 if args:
2265 parser.error('Unknown arguments: %s' % args)
2266
2267 cl = Changelist()
2268 if not cl.GetIssue():
2269 parser.error('Need to upload first')
2270
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002271 props = cl.GetIssueProperties()
2272 if props.get('private'):
2273 parser.error('Cannot use trybots with private issue')
2274
maruel@chromium.org15192402012-09-06 12:38:29 +00002275 if not options.name:
2276 options.name = cl.GetBranch()
2277
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002278 if options.bot and not options.master:
2279 parser.error('For manually specified bots please also specify the '
sergeyberezin@chromium.orgfbfecb72014-07-29 17:17:20 +00002280 'tryserver master, e.g. "-m tryserver.chromium.linux".')
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002281
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002282 def GetMasterMap():
2283 # Process --bot and --testfilter.
2284 if not options.bot:
2285 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002286
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002287 # Get try masters from PRESUBMIT.py files.
2288 masters = presubmit_support.DoGetTryMasters(
2289 change,
2290 change.LocalPaths(),
2291 settings.GetRoot(),
2292 None,
2293 None,
2294 options.verbose,
2295 sys.stdout)
2296 if masters:
2297 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002298
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002299 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2300 options.bot = presubmit_support.DoGetTrySlaves(
2301 change,
2302 change.LocalPaths(),
2303 settings.GetRoot(),
2304 None,
2305 None,
2306 options.verbose,
2307 sys.stdout)
2308 if not options.bot:
2309 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002310
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002311 builders_and_tests = {}
2312 # TODO(machenbach): The old style command-line options don't support
2313 # multiple try masters yet.
2314 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2315 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2316
2317 for bot in old_style:
2318 if ':' in bot:
2319 builder, tests = bot.split(':', 1)
2320 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2321 elif ',' in bot:
2322 parser.error('Specify one bot per --bot flag')
2323 else:
2324 builders_and_tests.setdefault(bot, []).append('defaulttests')
2325
2326 for bot, tests in new_style:
2327 builders_and_tests.setdefault(bot, []).extend(tests)
2328
2329 # Return a master map with one master to be backwards compatible. The
2330 # master name defaults to an empty string, which will cause the master
2331 # not to be set on rietveld (deprecated).
2332 return {options.master: builders_and_tests}
2333
2334 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002335
maruel@chromium.org15192402012-09-06 12:38:29 +00002336 if options.testfilter:
2337 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002338 masters = dict((master, dict(
2339 (b, forced_tests) for b, t in slaves.iteritems()
2340 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002341
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002342 for builders in masters.itervalues():
2343 if any('triggered' in b for b in builders):
2344 print >> sys.stderr, (
2345 'ERROR You are trying to send a job to a triggered bot. This type of'
2346 ' bot requires an\ninitial job from a parent (usually a builder). '
2347 'Instead send your job to the parent.\n'
2348 'Bot list: %s' % builders)
2349 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002350
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002351 patchset = cl.GetMostRecentPatchset()
2352 if patchset and patchset != cl.GetPatchset():
2353 print(
2354 '\nWARNING Mismatch between local config and server. Did a previous '
2355 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2356 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002357 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002358 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002359 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002360 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002361 except urllib2.HTTPError, e:
2362 if e.code == 404:
2363 print('404 from rietveld; '
2364 'did you mean to use "git try" instead of "git cl try"?')
2365 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002366 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002367
2368 for (master, builders) in masters.iteritems():
2369 if master:
2370 print 'Master: %s' % master
2371 length = max(len(builder) for builder in builders)
2372 for builder in sorted(builders):
2373 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002374 return 0
2375
2376
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002377@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002378def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002379 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002380 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002381 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002382 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002383
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002384 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002385 if args:
2386 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002387 branch = cl.GetBranch()
2388 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002389 cl = Changelist()
2390 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002391
2392 # Clear configured merge-base, if there is one.
2393 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002394 else:
2395 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002396 return 0
2397
2398
thestig@chromium.org00858c82013-12-02 23:08:03 +00002399def CMDweb(parser, args):
2400 """Opens the current CL in the web browser."""
2401 _, args = parser.parse_args(args)
2402 if args:
2403 parser.error('Unrecognized args: %s' % ' '.join(args))
2404
2405 issue_url = Changelist().GetIssueURL()
2406 if not issue_url:
2407 print >> sys.stderr, 'ERROR No issue to open'
2408 return 1
2409
2410 webbrowser.open(issue_url)
2411 return 0
2412
2413
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002414def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002415 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002416 _, args = parser.parse_args(args)
2417 if args:
2418 parser.error('Unrecognized args: %s' % ' '.join(args))
2419 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002420 props = cl.GetIssueProperties()
2421 if props.get('private'):
2422 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002423 cl.SetFlag('commit', '1')
2424 return 0
2425
2426
groby@chromium.org411034a2013-02-26 15:12:01 +00002427def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002428 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002429 _, args = parser.parse_args(args)
2430 if args:
2431 parser.error('Unrecognized args: %s' % ' '.join(args))
2432 cl = Changelist()
2433 # Ensure there actually is an issue to close.
2434 cl.GetDescription()
2435 cl.CloseIssue()
2436 return 0
2437
2438
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002439def CMDdiff(parser, args):
2440 """shows differences between local tree and last upload."""
2441 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002442 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002443 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002444 if not issue:
2445 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002446 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002447 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002448
2449 # Create a new branch based on the merge-base
2450 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2451 try:
2452 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002453 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002454 if rtn != 0:
2455 return rtn
2456
2457 # Switch back to starting brand and diff against the temporary
2458 # branch containing the latest rietveld patch.
2459 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2460 finally:
2461 RunGit(['checkout', '-q', branch])
2462 RunGit(['branch', '-D', TMP_BRANCH])
2463
2464 return 0
2465
2466
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002467def CMDowners(parser, args):
2468 """interactively find the owners for reviewing"""
2469 parser.add_option(
2470 '--no-color',
2471 action='store_true',
2472 help='Use this option to disable color output')
2473 options, args = parser.parse_args(args)
2474
2475 author = RunGit(['config', 'user.email']).strip() or None
2476
2477 cl = Changelist()
2478
2479 if args:
2480 if len(args) > 1:
2481 parser.error('Unknown args')
2482 base_branch = args[0]
2483 else:
2484 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002485 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002486
2487 change = cl.GetChange(base_branch, None)
2488 return owners_finder.OwnersFinder(
2489 [f.LocalPath() for f in
2490 cl.GetChange(base_branch, None).AffectedFiles()],
2491 change.RepositoryRoot(), author,
2492 fopen=file, os_path=os.path, glob=glob.glob,
2493 disable_color=options.no_color).run()
2494
2495
enne@chromium.org555cfe42014-01-29 18:21:39 +00002496@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002497def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002498 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002499 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002500 parser.add_option('--full', action='store_true',
2501 help='Reformat the full content of all touched files')
2502 parser.add_option('--dry-run', action='store_true',
2503 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002504 parser.add_option('--diff', action='store_true',
2505 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002506 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002507
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002508 # git diff generates paths against the root of the repository. Change
2509 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002510 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002511 if rel_base_path:
2512 os.chdir(rel_base_path)
2513
digit@chromium.org29e47272013-05-17 17:01:46 +00002514 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002515 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002516 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002517 # Only list the names of modified files.
2518 diff_cmd.append('--name-only')
2519 else:
2520 # Only generate context-less patches.
2521 diff_cmd.append('-U0')
2522
2523 # Grab the merge-base commit, i.e. the upstream commit of the current
2524 # branch when it was created or the last time it was rebased. This is
2525 # to cover the case where the user may have called "git fetch origin",
2526 # moving the origin branch to a newer commit, but hasn't rebased yet.
2527 upstream_commit = None
2528 cl = Changelist()
2529 upstream_branch = cl.GetUpstreamBranch()
2530 if upstream_branch:
2531 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2532 upstream_commit = upstream_commit.strip()
2533
2534 if not upstream_commit:
2535 DieWithError('Could not find base commit for this branch. '
2536 'Are you in detached state?')
2537
2538 diff_cmd.append(upstream_commit)
2539
2540 # Handle source file filtering.
2541 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002542 if args:
2543 for arg in args:
2544 if os.path.isdir(arg):
2545 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2546 elif os.path.isfile(arg):
2547 diff_cmd.append(arg)
2548 else:
2549 DieWithError('Argument "%s" is not a file or a directory' % arg)
2550 else:
2551 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002552 diff_output = RunGit(diff_cmd)
2553
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002554 top_dir = os.path.normpath(
2555 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2556
2557 # Locate the clang-format binary in the checkout
2558 try:
2559 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2560 except clang_format.NotFoundError, e:
2561 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002562
digit@chromium.org29e47272013-05-17 17:01:46 +00002563 if opts.full:
2564 # diff_output is a list of files to send to clang-format.
2565 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002566 if not files:
2567 print "Nothing to format."
2568 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002569 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002570 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002571 cmd.append('-i')
2572 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002573 if opts.diff:
2574 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002575 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002576 env = os.environ.copy()
2577 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002578 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002579 try:
2580 script = clang_format.FindClangFormatScriptInChromiumTree(
2581 'clang-format-diff.py')
2582 except clang_format.NotFoundError, e:
2583 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002584
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002585 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002586 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002587 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002588
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002589 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002590 if opts.diff:
2591 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002592 if opts.dry_run and len(stdout) > 0:
2593 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002594
2595 return 0
2596
2597
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002598class OptionParser(optparse.OptionParser):
2599 """Creates the option parse and add --verbose support."""
2600 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002601 optparse.OptionParser.__init__(
2602 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002603 self.add_option(
2604 '-v', '--verbose', action='count', default=0,
2605 help='Use 2 times for more debugging info')
2606
2607 def parse_args(self, args=None, values=None):
2608 options, args = optparse.OptionParser.parse_args(self, args, values)
2609 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2610 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2611 return options, args
2612
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002613
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002614def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002615 if sys.hexversion < 0x02060000:
2616 print >> sys.stderr, (
2617 '\nYour python version %s is unsupported, please upgrade.\n' %
2618 sys.version.split(' ', 1)[0])
2619 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002620
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002621 # Reload settings.
2622 global settings
2623 settings = Settings()
2624
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002625 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002626 dispatcher = subcommand.CommandDispatcher(__name__)
2627 try:
2628 return dispatcher.execute(OptionParser(), argv)
2629 except urllib2.HTTPError, e:
2630 if e.code != 500:
2631 raise
2632 DieWithError(
2633 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2634 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002635
2636
2637if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002638 # These affect sys.stdout so do it outside of main() to simplify mocks in
2639 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002640 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002641 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002642 sys.exit(main(sys.argv[1:]))