blob: 3c51377f61d722d46091d2ea40030b479534eef3 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000010import datetime
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000013import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import logging
15import optparse
16import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000017import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import textwrap
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000022import time
maruel@chromium.org1033efd2013-07-23 23:25:09 +000023import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000025import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000026import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027
28try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000029 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030except ImportError:
31 pass
32
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035from third_party import upload
36import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
41import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000043import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000046import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047import watchlists
48
maruel@chromium.org0633fb42013-08-16 20:06:14 +000049__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000051DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000052POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000054GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000055CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056
thestig@chromium.org44202a22014-03-11 19:22:18 +000057# Valid extensions for files we want to lint.
58DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
59DEFAULT_LINT_IGNORE_REGEX = r"$^"
60
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000061# Shortcut since it quickly becomes redundant.
62Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000063
maruel@chromium.orgddd59412011-11-30 14:20:38 +000064# Initialized in main()
65settings = None
66
67
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000069 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070 sys.exit(1)
71
72
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000073def GetNoGitPagerEnv():
74 env = os.environ.copy()
75 # 'cat' is a magical git string that disables pagers on all platforms.
76 env['GIT_PAGER'] = 'cat'
77 return env
78
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000081 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000082 except subprocess2.CalledProcessError as e:
83 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000086 'Command "%s" failed.\n%s' % (
87 ' '.join(args), error_message or e.stdout or ''))
88 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089
90
91def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000092 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000093 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000094
95
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000096def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000098 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000099 if suppress_stderr:
100 stderr = subprocess2.VOID
101 else:
102 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000103 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000104 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000105 stdout=subprocess2.PIPE,
106 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000107 return code, out[0]
108 except ValueError:
109 # When the subprocess fails, it returns None. That triggers a ValueError
110 # when trying to unpack the return value into (out, code).
111 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000114def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000115 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000117 return (version.startswith(prefix) and
118 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000119
120
maruel@chromium.org90541732011-04-01 17:54:18 +0000121def ask_for_data(prompt):
122 try:
123 return raw_input(prompt)
124 except KeyboardInterrupt:
125 # Hide the exception.
126 sys.exit(1)
127
128
iannucci@chromium.org79540052012-10-19 23:15:26 +0000129def git_set_branch_value(key, value):
130 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000131 if not branch:
132 return
133
134 cmd = ['config']
135 if isinstance(value, int):
136 cmd.append('--int')
137 git_key = 'branch.%s.%s' % (branch, key)
138 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139
140
141def git_get_branch_default(key, default):
142 branch = Changelist().GetBranch()
143 if branch:
144 git_key = 'branch.%s.%s' % (branch, key)
145 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
146 try:
147 return int(stdout.strip())
148 except ValueError:
149 pass
150 return default
151
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153def add_git_similarity(parser):
154 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 help='Sets the percentage that a pair of files need to match in order to'
157 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158 parser.add_option(
159 '--find-copies', action='store_true',
160 help='Allows git to look for copies.')
161 parser.add_option(
162 '--no-find-copies', action='store_false', dest='find_copies',
163 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164
165 old_parser_args = parser.parse_args
166 def Parse(args):
167 options, args = old_parser_args(args)
168
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000171 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 print('Note: Saving similarity of %d%% in git config.'
173 % options.similarity)
174 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000175
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 options.similarity = max(0, min(options.similarity, 100))
177
178 if options.find_copies is None:
179 options.find_copies = bool(
180 git_get_branch_default('git-find-copies', True))
181 else:
182 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183
184 print('Using %d%% similarity for rename/copy detection. '
185 'Override with --similarity.' % options.similarity)
186
187 return options, args
188 parser.parse_args = Parse
189
190
ukai@chromium.org259e4682012-10-25 07:36:33 +0000191def is_dirty_git_tree(cmd):
192 # Make sure index is up-to-date before running diff-index.
193 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
194 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
195 if dirty:
196 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
197 print 'Uncommitted files: (git diff-index --name-status HEAD)'
198 print dirty[:4096]
199 if len(dirty) > 4096:
200 print '... (run "git diff-index --name-status HEAD" to see full output).'
201 return True
202 return False
203
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000204
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000205def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
206 """Return the corresponding git ref if |base_url| together with |glob_spec|
207 matches the full |url|.
208
209 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
210 """
211 fetch_suburl, as_ref = glob_spec.split(':')
212 if allow_wildcards:
213 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
214 if glob_match:
215 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
216 # "branches/{472,597,648}/src:refs/remotes/svn/*".
217 branch_re = re.escape(base_url)
218 if glob_match.group(1):
219 branch_re += '/' + re.escape(glob_match.group(1))
220 wildcard = glob_match.group(2)
221 if wildcard == '*':
222 branch_re += '([^/]*)'
223 else:
224 # Escape and replace surrounding braces with parentheses and commas
225 # with pipe symbols.
226 wildcard = re.escape(wildcard)
227 wildcard = re.sub('^\\\\{', '(', wildcard)
228 wildcard = re.sub('\\\\,', '|', wildcard)
229 wildcard = re.sub('\\\\}$', ')', wildcard)
230 branch_re += wildcard
231 if glob_match.group(3):
232 branch_re += re.escape(glob_match.group(3))
233 match = re.match(branch_re, url)
234 if match:
235 return re.sub('\*$', match.group(1), as_ref)
236
237 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
238 if fetch_suburl:
239 full_url = base_url + '/' + fetch_suburl
240 else:
241 full_url = base_url
242 if full_url == url:
243 return as_ref
244 return None
245
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000246
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 """Prints statistics about the change to the user."""
249 # --no-ext-diff is broken in some versions of Git, so try to work around
250 # this by overriding the environment (but there is still a problem if the
251 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000252 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000253 if 'GIT_EXTERNAL_DIFF' in env:
254 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255
256 if find_copies:
257 similarity_options = ['--find-copies-harder', '-l100000',
258 '-C%s' % similarity]
259 else:
260 similarity_options = ['-M%s' % similarity]
261
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000262 try:
263 stdout = sys.stdout.fileno()
264 except AttributeError:
265 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000266 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000267 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000268 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000269 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000270
271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272class Settings(object):
273 def __init__(self):
274 self.default_server = None
275 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000276 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 self.is_git_svn = None
278 self.svn_branch = None
279 self.tree_status_url = None
280 self.viewvc_url = None
281 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000282 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000283 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000284 self.project = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285
286 def LazyUpdateIfNeeded(self):
287 """Updates the settings from a codereview.settings file, if available."""
288 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000289 # The only value that actually changes the behavior is
290 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000291 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000292 error_ok=True
293 ).strip().lower()
294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000296 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000298 # set updated to True to avoid infinite calling loop
299 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000300 self.updated = True
301 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 self.updated = True
303
304 def GetDefaultServerUrl(self, error_ok=False):
305 if not self.default_server:
306 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000307 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000308 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309 if error_ok:
310 return self.default_server
311 if not self.default_server:
312 error_message = ('Could not find settings file. You must configure '
313 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000314 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000315 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316 return self.default_server
317
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000318 @staticmethod
319 def GetRelativeRoot():
320 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000323 if self.root is None:
324 self.root = os.path.abspath(self.GetRelativeRoot())
325 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326
327 def GetIsGitSvn(self):
328 """Return true if this repo looks like it's using git-svn."""
329 if self.is_git_svn is None:
bratell@opera.com8e05af12014-07-07 07:28:34 +0000330 # The presence of a svn-remote using the svn:// (or file://)
331 # protocol suggests that you're using svn. Remotes with the
332 # http* protocols suggest read-only svn access and are ignored.
333 code, result = RunGitWithCode(
334 ['config', '--local', '--get-regexp', r'^svn-remote\..*\.url'])
335 self.is_git_svn = (code == 0 and ("svn://" in result or
336 "file://" in result))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337 return self.is_git_svn
338
339 def GetSVNBranch(self):
340 if self.svn_branch is None:
341 if not self.GetIsGitSvn():
342 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
343
344 # Try to figure out which remote branch we're based on.
345 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000346 # 1) iterate through our branch history and find the svn URL.
347 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348
349 # regexp matching the git-svn line that contains the URL.
350 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
351
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000352 # We don't want to go through all of history, so read a line from the
353 # pipe at a time.
354 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000355 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000356 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
357 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000358 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000359 for line in proc.stdout:
360 match = git_svn_re.match(line)
361 if match:
362 url = match.group(1)
363 proc.stdout.close() # Cut pipe.
364 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000366 if url:
367 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
368 remotes = RunGit(['config', '--get-regexp',
369 r'^svn-remote\..*\.url']).splitlines()
370 for remote in remotes:
371 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000373 remote = match.group(1)
374 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000375 rewrite_root = RunGit(
376 ['config', 'svn-remote.%s.rewriteRoot' % remote],
377 error_ok=True).strip()
378 if rewrite_root:
379 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000380 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000381 ['config', 'svn-remote.%s.fetch' % remote],
382 error_ok=True).strip()
383 if fetch_spec:
384 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
385 if self.svn_branch:
386 break
387 branch_spec = RunGit(
388 ['config', 'svn-remote.%s.branches' % remote],
389 error_ok=True).strip()
390 if branch_spec:
391 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
392 if self.svn_branch:
393 break
394 tag_spec = RunGit(
395 ['config', 'svn-remote.%s.tags' % remote],
396 error_ok=True).strip()
397 if tag_spec:
398 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
399 if self.svn_branch:
400 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401
402 if not self.svn_branch:
403 DieWithError('Can\'t guess svn branch -- try specifying it on the '
404 'command line')
405
406 return self.svn_branch
407
408 def GetTreeStatusUrl(self, error_ok=False):
409 if not self.tree_status_url:
410 error_message = ('You must configure your tree status URL by running '
411 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000412 self.tree_status_url = self._GetRietveldConfig(
413 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 return self.tree_status_url
415
416 def GetViewVCUrl(self):
417 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000418 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 return self.viewvc_url
420
rmistry@google.com90752582014-01-14 21:04:50 +0000421 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000422 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000423
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000424 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000425 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000426
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000427 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000428 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000429
ukai@chromium.orge8077812012-02-03 03:41:46 +0000430 def GetIsGerrit(self):
431 """Return true if this repo is assosiated with gerrit code review system."""
432 if self.is_gerrit is None:
433 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
434 return self.is_gerrit
435
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000436 def GetGitEditor(self):
437 """Return the editor specified in the git config, or None if none is."""
438 if self.git_editor is None:
439 self.git_editor = self._GetConfig('core.editor', error_ok=True)
440 return self.git_editor or None
441
thestig@chromium.org44202a22014-03-11 19:22:18 +0000442 def GetLintRegex(self):
443 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
444 DEFAULT_LINT_REGEX)
445
446 def GetLintIgnoreRegex(self):
447 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
448 DEFAULT_LINT_IGNORE_REGEX)
449
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000450 def GetProject(self):
451 if not self.project:
452 self.project = self._GetRietveldConfig('project', error_ok=True)
453 return self.project
454
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000455 def _GetRietveldConfig(self, param, **kwargs):
456 return self._GetConfig('rietveld.' + param, **kwargs)
457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 def _GetConfig(self, param, **kwargs):
459 self.LazyUpdateIfNeeded()
460 return RunGit(['config', param], **kwargs).strip()
461
462
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000463def ShortBranchName(branch):
464 """Convert a name like 'refs/heads/foo' to just 'foo'."""
465 return branch.replace('refs/heads/', '')
466
467
468class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000469 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000471 global settings
472 if not settings:
473 # Happens when git_cl.py is used as a utility library.
474 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000475 settings.GetDefaultServerUrl()
476 self.branchref = branchref
477 if self.branchref:
478 self.branch = ShortBranchName(self.branchref)
479 else:
480 self.branch = None
481 self.rietveld_server = None
482 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000483 self.lookedup_issue = False
484 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000485 self.has_description = False
486 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000487 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000488 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000489 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000490 self.cc = None
491 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000492 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000493 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000494
495 def GetCCList(self):
496 """Return the users cc'd on this CL.
497
498 Return is a string suitable for passing to gcl with the --cc flag.
499 """
500 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000501 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000502 more_cc = ','.join(self.watchers)
503 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
504 return self.cc
505
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000506 def GetCCListWithoutDefault(self):
507 """Return the users cc'd on this CL excluding default ones."""
508 if self.cc is None:
509 self.cc = ','.join(self.watchers)
510 return self.cc
511
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000512 def SetWatchers(self, watchers):
513 """Set the list of email addresses that should be cc'd based on the changed
514 files in this CL.
515 """
516 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000517
518 def GetBranch(self):
519 """Returns the short branch name, e.g. 'master'."""
520 if not self.branch:
521 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
522 self.branch = ShortBranchName(self.branchref)
523 return self.branch
524
525 def GetBranchRef(self):
526 """Returns the full branch name, e.g. 'refs/heads/master'."""
527 self.GetBranch() # Poke the lazy loader.
528 return self.branchref
529
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000530 @staticmethod
531 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000532 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000533 e.g. 'origin', 'refs/heads/master'
534 """
535 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000536 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
537 error_ok=True).strip()
538 if upstream_branch:
539 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
540 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000541 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
542 error_ok=True).strip()
543 if upstream_branch:
544 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000545 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000546 # Fall back on trying a git-svn upstream branch.
547 if settings.GetIsGitSvn():
548 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000550 # Else, try to guess the origin remote.
551 remote_branches = RunGit(['branch', '-r']).split()
552 if 'origin/master' in remote_branches:
553 # Fall back on origin/master if it exits.
554 remote = 'origin'
555 upstream_branch = 'refs/heads/master'
556 elif 'origin/trunk' in remote_branches:
557 # Fall back on origin/trunk if it exists. Generally a shared
558 # git-svn clone
559 remote = 'origin'
560 upstream_branch = 'refs/heads/trunk'
561 else:
562 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000563Either pass complete "git diff"-style arguments, like
564 git cl upload origin/master
565or verify this branch is set up to track another (via the --track argument to
566"git checkout -b ...").""")
567
568 return remote, upstream_branch
569
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000570 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000571 return git_common.get_or_create_merge_base(self.GetBranch(),
572 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000573
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574 def GetUpstreamBranch(self):
575 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000576 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000577 if remote is not '.':
578 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
579 self.upstream_branch = upstream_branch
580 return self.upstream_branch
581
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000582 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000583 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000584 remote, branch = None, self.GetBranch()
585 seen_branches = set()
586 while branch not in seen_branches:
587 seen_branches.add(branch)
588 remote, branch = self.FetchUpstreamTuple(branch)
589 branch = ShortBranchName(branch)
590 if remote != '.' or branch.startswith('refs/remotes'):
591 break
592 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000593 remotes = RunGit(['remote'], error_ok=True).split()
594 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000595 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000596 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000597 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000598 logging.warning('Could not determine which remote this change is '
599 'associated with, so defaulting to "%s". This may '
600 'not be what you want. You may prevent this message '
601 'by running "git svn info" as documented here: %s',
602 self._remote,
603 GIT_INSTRUCTIONS_URL)
604 else:
605 logging.warn('Could not determine which remote this change is '
606 'associated with. You may prevent this message by '
607 'running "git svn info" as documented here: %s',
608 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000609 branch = 'HEAD'
610 if branch.startswith('refs/remotes'):
611 self._remote = (remote, branch)
612 else:
613 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000614 return self._remote
615
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000616 def GitSanityChecks(self, upstream_git_obj):
617 """Checks git repo status and ensures diff is from local commits."""
618
619 # Verify the commit we're diffing against is in our current branch.
620 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
621 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
622 if upstream_sha != common_ancestor:
623 print >> sys.stderr, (
624 'ERROR: %s is not in the current branch. You may need to rebase '
625 'your tracking branch' % upstream_sha)
626 return False
627
628 # List the commits inside the diff, and verify they are all local.
629 commits_in_diff = RunGit(
630 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
631 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
632 remote_branch = remote_branch.strip()
633 if code != 0:
634 _, remote_branch = self.GetRemoteBranch()
635
636 commits_in_remote = RunGit(
637 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
638
639 common_commits = set(commits_in_diff) & set(commits_in_remote)
640 if common_commits:
641 print >> sys.stderr, (
642 'ERROR: Your diff contains %d commits already in %s.\n'
643 'Run "git log --oneline %s..HEAD" to get a list of commits in '
644 'the diff. If you are using a custom git flow, you can override'
645 ' the reference used for this check with "git config '
646 'gitcl.remotebranch <git-ref>".' % (
647 len(common_commits), remote_branch, upstream_git_obj))
648 return False
649 return True
650
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000651 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000652 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000653
654 Returns None if it is not set.
655 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000656 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
657 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000658
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 def GetRemoteUrl(self):
660 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
661
662 Returns None if there is no remote.
663 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000664 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000665 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
666
667 # If URL is pointing to a local directory, it is probably a git cache.
668 if os.path.isdir(url):
669 url = RunGit(['config', 'remote.%s.url' % remote],
670 error_ok=True,
671 cwd=url).strip()
672 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673
674 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000675 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000676 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000678 self.issue = int(issue) or None if issue else None
679 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 return self.issue
681
682 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000683 if not self.rietveld_server:
684 # If we're on a branch then get the server potentially associated
685 # with that branch.
686 if self.GetIssue():
687 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
688 ['config', self._RietveldServer()], error_ok=True).strip())
689 if not self.rietveld_server:
690 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 return self.rietveld_server
692
693 def GetIssueURL(self):
694 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000695 if not self.GetIssue():
696 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
698
699 def GetDescription(self, pretty=False):
700 if not self.has_description:
701 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000702 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000703 try:
704 self.description = self.RpcServer().get_description(issue).strip()
705 except urllib2.HTTPError, e:
706 if e.code == 404:
707 DieWithError(
708 ('\nWhile fetching the description for issue %d, received a '
709 '404 (not found)\n'
710 'error. It is likely that you deleted this '
711 'issue on the server. If this is the\n'
712 'case, please run\n\n'
713 ' git cl issue 0\n\n'
714 'to clear the association with the deleted issue. Then run '
715 'this command again.') % issue)
716 else:
717 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000718 '\nFailed to fetch issue description. HTTP error %d' % e.code)
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())
1323 print 'Issue description:'
1324 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325 return 0
1326
1327
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001328def colorize_CMDstatus_doc():
1329 """To be called once in main() to add colors to git cl status help."""
1330 colors = [i for i in dir(Fore) if i[0].isupper()]
1331
1332 def colorize_line(line):
1333 for color in colors:
1334 if color in line.upper():
1335 # Extract whitespaces first and the leading '-'.
1336 indent = len(line) - len(line.lstrip(' ')) + 1
1337 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1338 return line
1339
1340 lines = CMDstatus.__doc__.splitlines()
1341 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1342
1343
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001344@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001345def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001346 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347
1348 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001349 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001350 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351
1352 cl = Changelist()
1353 if len(args) > 0:
1354 try:
1355 issue = int(args[0])
1356 except ValueError:
1357 DieWithError('Pass a number to set the issue or none to list it.\n'
1358 'Maybe you want to run git cl status?')
1359 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001360 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361 return 0
1362
1363
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001364def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001365 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001366 (_, args) = parser.parse_args(args)
1367 if args:
1368 parser.error('Unsupported argument: %s' % args)
1369
1370 cl = Changelist()
1371 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001372 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001373 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001374 if message['disapproval']:
1375 color = Fore.RED
1376 elif message['approval']:
1377 color = Fore.GREEN
1378 elif message['sender'] == data['owner_email']:
1379 color = Fore.MAGENTA
1380 else:
1381 color = Fore.BLUE
1382 print '\n%s%s %s%s' % (
1383 color, message['date'].split('.', 1)[0], message['sender'],
1384 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001385 if message['text'].strip():
1386 print '\n'.join(' ' + l for l in message['text'].splitlines())
1387 return 0
1388
1389
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001390def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001391 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001392 cl = Changelist()
1393 if not cl.GetIssue():
1394 DieWithError('This branch has no associated changelist.')
1395 description = ChangeDescription(cl.GetDescription())
1396 description.prompt()
1397 cl.UpdateDescription(description.description)
1398 return 0
1399
1400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401def CreateDescriptionFromLog(args):
1402 """Pulls out the commit log to use as a base for the CL description."""
1403 log_args = []
1404 if len(args) == 1 and not args[0].endswith('.'):
1405 log_args = [args[0] + '..']
1406 elif len(args) == 1 and args[0].endswith('...'):
1407 log_args = [args[0][:-1]]
1408 elif len(args) == 2:
1409 log_args = [args[0] + '..' + args[1]]
1410 else:
1411 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001412 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
1414
thestig@chromium.org44202a22014-03-11 19:22:18 +00001415def CMDlint(parser, args):
1416 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001417 parser.add_option('--filter', action='append', metavar='-x,+y',
1418 help='Comma-separated list of cpplint\'s category-filters')
1419 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001420
1421 # Access to a protected member _XX of a client class
1422 # pylint: disable=W0212
1423 try:
1424 import cpplint
1425 import cpplint_chromium
1426 except ImportError:
1427 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1428 return 1
1429
1430 # Change the current working directory before calling lint so that it
1431 # shows the correct base.
1432 previous_cwd = os.getcwd()
1433 os.chdir(settings.GetRoot())
1434 try:
1435 cl = Changelist()
1436 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1437 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001438 if not files:
1439 print "Cannot lint an empty CL"
1440 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001441
1442 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001443 command = args + files
1444 if options.filter:
1445 command = ['--filter=' + ','.join(options.filter)] + command
1446 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001447
1448 white_regex = re.compile(settings.GetLintRegex())
1449 black_regex = re.compile(settings.GetLintIgnoreRegex())
1450 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1451 for filename in filenames:
1452 if white_regex.match(filename):
1453 if black_regex.match(filename):
1454 print "Ignoring file %s" % filename
1455 else:
1456 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1457 extra_check_functions)
1458 else:
1459 print "Skipping file %s" % filename
1460 finally:
1461 os.chdir(previous_cwd)
1462 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1463 if cpplint._cpplint_state.error_count != 0:
1464 return 1
1465 return 0
1466
1467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001469 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001470 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001472 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001473 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 (options, args) = parser.parse_args(args)
1475
ukai@chromium.org259e4682012-10-25 07:36:33 +00001476 if not options.force and is_dirty_git_tree('presubmit'):
1477 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 return 1
1479
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001480 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481 if args:
1482 base_branch = args[0]
1483 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001484 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001485 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001487 cl.RunHook(
1488 committing=not options.upload,
1489 may_prompt=False,
1490 verbose=options.verbose,
1491 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001492 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493
1494
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001495def AddChangeIdToCommitMessage(options, args):
1496 """Re-commits using the current message, assumes the commit hook is in
1497 place.
1498 """
1499 log_desc = options.message or CreateDescriptionFromLog(args)
1500 git_command = ['commit', '--amend', '-m', log_desc]
1501 RunGit(git_command)
1502 new_log_desc = CreateDescriptionFromLog(args)
1503 if CHANGE_ID in new_log_desc:
1504 print 'git-cl: Added Change-Id to commit message.'
1505 else:
1506 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1507
1508
ukai@chromium.orge8077812012-02-03 03:41:46 +00001509def GerritUpload(options, args, cl):
1510 """upload the current branch to gerrit."""
1511 # We assume the remote called "origin" is the one we want.
1512 # It is probably not worthwhile to support different workflows.
1513 remote = 'origin'
1514 branch = 'master'
1515 if options.target_branch:
1516 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001517
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001518 change_desc = ChangeDescription(
1519 options.message or CreateDescriptionFromLog(args))
1520 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001521 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001522 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001523 if CHANGE_ID not in change_desc.description:
1524 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001525
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001526 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001527 if len(commits) > 1:
1528 print('WARNING: This will upload %d commits. Run the following command '
1529 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001530 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001531 print('You can also use `git squash-branch` to squash these into a single'
1532 'commit.')
1533 ask_for_data('About to upload; enter to confirm.')
1534
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001535 if options.reviewers:
1536 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537
ukai@chromium.orge8077812012-02-03 03:41:46 +00001538 receive_options = []
1539 cc = cl.GetCCList().split(',')
1540 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001541 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001542 cc = filter(None, cc)
1543 if cc:
1544 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001545 if change_desc.get_reviewers():
1546 receive_options.extend(
1547 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548
ukai@chromium.orge8077812012-02-03 03:41:46 +00001549 git_command = ['push']
1550 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001551 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001552 ' '.join(receive_options))
1553 git_command += [remote, 'HEAD:refs/for/' + branch]
1554 RunGit(git_command)
1555 # TODO(ukai): parse Change-Id: and set issue number?
1556 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001557
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001558
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001559def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001560 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001561 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1562 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 if options.emulate_svn_auto_props:
1564 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565
1566 change_desc = None
1567
pgervais@chromium.org91141372014-01-09 23:27:20 +00001568 if options.email is not None:
1569 upload_args.extend(['--email', options.email])
1570
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001572 if options.title:
1573 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001574 if options.message:
1575 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001576 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 print ("This branch is associated with issue %s. "
1578 "Adding patch to that issue." % cl.GetIssue())
1579 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001580 if options.title:
1581 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001582 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001583 change_desc = ChangeDescription(message)
1584 if options.reviewers:
1585 change_desc.update_reviewers(options.reviewers)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001586 if options.auto_bots:
1587 masters = presubmit_support.DoGetTryMasters(
1588 change,
1589 change.LocalPaths(),
1590 settings.GetRoot(),
1591 None,
1592 None,
1593 options.verbose,
1594 sys.stdout)
1595
1596 if masters:
1597 change_description = change_desc.description + '\nCQ_TRYBOTS='
1598 lst = []
1599 for master, mapping in masters.iteritems():
1600 lst.append(master + ':' + ','.join(mapping.keys()))
1601 change_desc.set_description(change_description + ';'.join(lst))
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001602 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001603 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001604
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001605 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001606 print "Description is empty; aborting."
1607 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001608
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001609 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001610 if change_desc.get_reviewers():
1611 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001612 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001613 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001614 DieWithError("Must specify reviewers to send email.")
1615 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001616
1617 # We check this before applying rietveld.private assuming that in
1618 # rietveld.cc only addresses which we can send private CLs to are listed
1619 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1620 # --private is specified explicitly on the command line.
1621 if options.private:
1622 logging.warn('rietveld.cc is ignored since private flag is specified. '
1623 'You need to review and add them manually if necessary.')
1624 cc = cl.GetCCListWithoutDefault()
1625 else:
1626 cc = cl.GetCCList()
1627 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001628 if cc:
1629 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001631 if options.private or settings.GetDefaultPrivateFlag() == "True":
1632 upload_args.append('--private')
1633
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001634 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001635 if not options.find_copies:
1636 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001638 # Include the upstream repo's URL in the change -- this is useful for
1639 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001640 remote_url = cl.GetGitBaseUrlFromConfig()
1641 if not remote_url:
1642 if settings.GetIsGitSvn():
1643 # URL is dependent on the current directory.
1644 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1645 if data:
1646 keys = dict(line.split(': ', 1) for line in data.splitlines()
1647 if ': ' in line)
1648 remote_url = keys.get('URL', None)
1649 else:
1650 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1651 remote_url = (cl.GetRemoteUrl() + '@'
1652 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 if remote_url:
1654 upload_args.extend(['--base_url', remote_url])
1655
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001656 project = settings.GetProject()
1657 if project:
1658 upload_args.extend(['--project', project])
1659
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001660 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001661 upload_args = ['upload'] + upload_args + args
1662 logging.info('upload.RealMain(%s)', upload_args)
1663 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001664 issue = int(issue)
1665 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001666 except KeyboardInterrupt:
1667 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001668 except:
1669 # If we got an exception after the user typed a description for their
1670 # change, back up the description before re-raising.
1671 if change_desc:
1672 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1673 print '\nGot exception while uploading -- saving description to %s\n' \
1674 % backup_path
1675 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001676 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677 backup_file.close()
1678 raise
1679
1680 if not cl.GetIssue():
1681 cl.SetIssue(issue)
1682 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001683
1684 if options.use_commit_queue:
1685 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686 return 0
1687
1688
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001689def cleanup_list(l):
1690 """Fixes a list so that comma separated items are put as individual items.
1691
1692 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1693 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1694 """
1695 items = sum((i.split(',') for i in l), [])
1696 stripped_items = (i.strip() for i in items)
1697 return sorted(filter(None, stripped_items))
1698
1699
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001700@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001701def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001702 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001703 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1704 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001705 parser.add_option('--bypass-watchlists', action='store_true',
1706 dest='bypass_watchlists',
1707 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001708 parser.add_option('-f', action='store_true', dest='force',
1709 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001710 parser.add_option('-m', dest='message', help='message for patchset')
1711 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001712 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001713 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001714 help='reviewer email addresses')
1715 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001716 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001717 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001718 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001719 help='send email to reviewer immediately')
1720 parser.add_option("--emulate_svn_auto_props", action="store_true",
1721 dest="emulate_svn_auto_props",
1722 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001723 parser.add_option('-c', '--use-commit-queue', action='store_true',
1724 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001725 parser.add_option('--private', action='store_true',
1726 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001727 parser.add_option('--target_branch',
1728 help='When uploading to gerrit, remote branch to '
1729 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001730 parser.add_option('--email', default=None,
1731 help='email address to use to connect to Rietveld')
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001732 parser.add_option('--auto-bots', default=False, action='store_true',
1733 help='Autogenerate which trybots to use for this CL')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001734
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001735 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001736 (options, args) = parser.parse_args(args)
1737
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001738 if options.target_branch and not settings.GetIsGerrit():
1739 parser.error('Use --target_branch for non gerrit repository.')
1740
ukai@chromium.org259e4682012-10-25 07:36:33 +00001741 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001742 return 1
1743
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001744 options.reviewers = cleanup_list(options.reviewers)
1745 options.cc = cleanup_list(options.cc)
1746
ukai@chromium.orge8077812012-02-03 03:41:46 +00001747 cl = Changelist()
1748 if args:
1749 # TODO(ukai): is it ok for gerrit case?
1750 base_branch = args[0]
1751 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001752 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001753 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001754 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001755
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001756 # Apply watchlists on upload.
1757 change = cl.GetChange(base_branch, None)
1758 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1759 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001760 if not options.bypass_watchlists:
1761 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001762
ukai@chromium.orge8077812012-02-03 03:41:46 +00001763 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001764 if options.reviewers:
1765 # Set the reviewer list now so that presubmit checks can access it.
1766 change_description = ChangeDescription(change.FullDescriptionText())
1767 change_description.update_reviewers(options.reviewers)
1768 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001769 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001770 may_prompt=not options.force,
1771 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001772 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001773 if not hook_results.should_continue():
1774 return 1
1775 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001776 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001777
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001778 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001779 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001780 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001781 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001782 print ('The last upload made from this repository was patchset #%d but '
1783 'the most recent patchset on the server is #%d.'
1784 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001785 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1786 'from another machine or branch the patch you\'re uploading now '
1787 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001788 ask_for_data('About to upload; enter to confirm.')
1789
iannucci@chromium.org79540052012-10-19 23:15:26 +00001790 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001791 if settings.GetIsGerrit():
1792 return GerritUpload(options, args, cl)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001793 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001794 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001795 git_set_branch_value('last-upload-hash',
1796 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001797
1798 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001799
1800
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001801def IsSubmoduleMergeCommit(ref):
1802 # When submodules are added to the repo, we expect there to be a single
1803 # non-git-svn merge commit at remote HEAD with a signature comment.
1804 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001805 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001806 return RunGit(cmd) != ''
1807
1808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001809def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001810 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001812 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001813 Updates changelog with metadata (e.g. pointer to review).
1814 Pushes/dcommits the code upstream.
1815 Updates review and closes.
1816 """
1817 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1818 help='bypass upload presubmit hook')
1819 parser.add_option('-m', dest='message',
1820 help="override review description")
1821 parser.add_option('-f', action='store_true', dest='force',
1822 help="force yes to questions (don't prompt)")
1823 parser.add_option('-c', dest='contributor',
1824 help="external contributor for patch (appended to " +
1825 "description and used as author for git). Should be " +
1826 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001827 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828 (options, args) = parser.parse_args(args)
1829 cl = Changelist()
1830
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001831 current = cl.GetBranch()
1832 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1833 if not settings.GetIsGitSvn() and remote == '.':
1834 print
1835 print 'Attempting to push branch %r into another local branch!' % current
1836 print
1837 print 'Either reparent this branch on top of origin/master:'
1838 print ' git reparent-branch --root'
1839 print
1840 print 'OR run `git rebase-update` if you think the parent branch is already'
1841 print 'committed.'
1842 print
1843 print ' Current parent: %r' % upstream_branch
1844 return 1
1845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 if not args or cmd == 'push':
1847 # Default to merging against our best guess of the upstream branch.
1848 args = [cl.GetUpstreamBranch()]
1849
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001850 if options.contributor:
1851 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1852 print "Please provide contibutor as 'First Last <email@example.com>'"
1853 return 1
1854
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001856 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001857
ukai@chromium.org259e4682012-10-25 07:36:33 +00001858 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859 return 1
1860
1861 # This rev-list syntax means "show all commits not in my branch that
1862 # are in base_branch".
1863 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1864 base_branch]).splitlines()
1865 if upstream_commits:
1866 print ('Base branch "%s" has %d commits '
1867 'not in this branch.' % (base_branch, len(upstream_commits)))
1868 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1869 return 1
1870
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001871 # This is the revision `svn dcommit` will commit on top of.
1872 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1873 '--pretty=format:%H'])
1874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001876 # If the base_head is a submodule merge commit, the first parent of the
1877 # base_head should be a git-svn commit, which is what we're interested in.
1878 base_svn_head = base_branch
1879 if base_has_submodules:
1880 base_svn_head += '^1'
1881
1882 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001883 if extra_commits:
1884 print ('This branch has %d additional commits not upstreamed yet.'
1885 % len(extra_commits.splitlines()))
1886 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1887 'before attempting to %s.' % (base_branch, cmd))
1888 return 1
1889
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001890 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001891 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001892 author = None
1893 if options.contributor:
1894 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001895 hook_results = cl.RunHook(
1896 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001897 may_prompt=not options.force,
1898 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001899 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001900 if not hook_results.should_continue():
1901 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902
1903 if cmd == 'dcommit':
1904 # Check the tree status if the tree status URL is set.
1905 status = GetTreeStatus()
1906 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001907 print('The tree is closed. Please wait for it to reopen. Use '
1908 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 return 1
1910 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001911 print('Unable to determine tree status. Please verify manually and '
1912 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001913 else:
1914 breakpad.SendStack(
1915 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001916 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1917 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001918 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001920 change_desc = ChangeDescription(options.message)
1921 if not change_desc.description and cl.GetIssue():
1922 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001924 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001925 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001926 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001927 else:
1928 print 'No description set.'
1929 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1930 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001932 # Keep a separate copy for the commit message, because the commit message
1933 # contains the link to the Rietveld issue, while the Rietveld message contains
1934 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001935 # Keep a separate copy for the commit message.
1936 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001937 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001938
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001939 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001940 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001941 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001943 commit_desc.append_footer('Patch from %s.' % options.contributor)
1944
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001945 print('Description:')
1946 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947
1948 branches = [base_branch, cl.GetBranchRef()]
1949 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001950 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001951 ask_for_data('About to commit; enter to confirm.')
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:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00002016 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
2017 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'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00002036 comment = "Committed patchset #%d manually as r%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
2060to run 'git cl push' instead.
2061Choose 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
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002078@subcommand.usage('[upstream branch to apply against]')
2079def CMDpush(parser, args):
2080 """Temporary alias for 'land'.
2081 """
2082 print(
2083 "\n=======\n"
2084 "'git cl push' has been renamed to 'git cl land'.\n"
2085 "Currently they are treated as synonyms, but 'git cl push' will stop\n"
2086 "working after 2014/07/01.\n"
2087 "=======\n")
2088 now = datetime.datetime.utcfromtimestamp(time.time())
2089 if now > datetime.datetime(2014, 7, 1):
2090 print('******\nReally, you should not use this command anymore... \n'
2091 'Pausing 10 sec to help you remember :-)')
2092 for n in xrange(10):
2093 time.sleep(1)
2094 print('%s seconds...' % (n+1))
2095 print('******')
2096 return CMDland(parser, args)
2097
2098
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002099@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002100def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002101 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102 parser.add_option('-b', dest='newbranch',
2103 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002104 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002105 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002106 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2107 help='Change to the directory DIR immediately, '
2108 'before doing anything else.')
2109 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002110 help='failed patches spew .rej files rather than '
2111 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002112 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2113 help="don't commit after patch applies")
2114 (options, args) = parser.parse_args(args)
2115 if len(args) != 1:
2116 parser.print_help()
2117 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002118 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002120 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002121 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002122
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002123 if options.newbranch:
2124 if options.force:
2125 RunGit(['branch', '-D', options.newbranch],
2126 stderr=subprocess2.PIPE, error_ok=True)
2127 RunGit(['checkout', '-b', options.newbranch,
2128 Changelist().GetUpstreamBranch()])
2129
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002130 return PatchIssue(issue_arg, options.reject, options.nocommit,
2131 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002132
2133
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002134def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002135 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002137 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002138 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002139 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002140 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002142 # Assume it's a URL to the patch. Default to https.
2143 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002144 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002145 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002146 DieWithError('Must pass an issue ID or full URL for '
2147 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002148 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002149 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002150 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 # Switch up to the top-level directory, if necessary, in preparation for
2153 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002154 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002155 if top:
2156 os.chdir(top)
2157
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158 # Git patches have a/ at the beginning of source paths. We strip that out
2159 # with a sed script rather than the -p flag to patch so we can feed either
2160 # Git or svn-style patches into the same apply command.
2161 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002162 try:
2163 patch_data = subprocess2.check_output(
2164 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2165 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002166 DieWithError('Git patch mungling failed.')
2167 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002168
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002169 # We use "git apply" to apply the patch instead of "patch" so that we can
2170 # pick up file adds.
2171 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002172 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002173 if directory:
2174 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002175 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002176 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002177 elif IsGitVersionAtLeast('1.7.12'):
2178 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002179 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002180 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002181 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002182 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183 DieWithError('Failed to apply the patch')
2184
2185 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002186 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2188 cl = Changelist()
2189 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002190 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002191 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192 else:
2193 print "Patch applied to index."
2194 return 0
2195
2196
2197def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002198 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002199 # Provide a wrapper for git svn rebase to help avoid accidental
2200 # git svn dcommit.
2201 # It's the only command that doesn't use parser at all since we just defer
2202 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002203
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002204 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002205
2206
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002207def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002208 """Fetches the tree status and returns either 'open', 'closed',
2209 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002210 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002211 if url:
2212 status = urllib2.urlopen(url).read().lower()
2213 if status.find('closed') != -1 or status == '0':
2214 return 'closed'
2215 elif status.find('open') != -1 or status == '1':
2216 return 'open'
2217 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002218 return 'unset'
2219
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002221def GetTreeStatusReason():
2222 """Fetches the tree status from a json url and returns the message
2223 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002224 url = settings.GetTreeStatusUrl()
2225 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002226 connection = urllib2.urlopen(json_url)
2227 status = json.loads(connection.read())
2228 connection.close()
2229 return status['message']
2230
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002232def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002233 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002234 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002235 status = GetTreeStatus()
2236 if 'unset' == status:
2237 print 'You must configure your tree status URL by running "git cl config".'
2238 return 2
2239
2240 print "The tree is %s" % status
2241 print
2242 print GetTreeStatusReason()
2243 if status != 'open':
2244 return 1
2245 return 0
2246
2247
maruel@chromium.org15192402012-09-06 12:38:29 +00002248def CMDtry(parser, args):
2249 """Triggers a try job through Rietveld."""
2250 group = optparse.OptionGroup(parser, "Try job options")
2251 group.add_option(
2252 "-b", "--bot", action="append",
2253 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2254 "times to specify multiple builders. ex: "
2255 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2256 "the try server waterfall for the builders name and the tests "
2257 "available. Can also be used to specify gtest_filter, e.g. "
2258 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2259 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002260 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002261 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002262 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002263 "-r", "--revision",
2264 help="Revision to use for the try job; default: the "
2265 "revision will be determined by the try server; see "
2266 "its waterfall for more info")
2267 group.add_option(
2268 "-c", "--clobber", action="store_true", default=False,
2269 help="Force a clobber before building; e.g. don't do an "
2270 "incremental build")
2271 group.add_option(
2272 "--project",
2273 help="Override which project to use. Projects are defined "
2274 "server-side to define what default bot set to use")
2275 group.add_option(
2276 "-t", "--testfilter", action="append", default=[],
2277 help=("Apply a testfilter to all the selected builders. Unless the "
2278 "builders configurations are similar, use multiple "
2279 "--bot <builder>:<test> arguments."))
2280 group.add_option(
2281 "-n", "--name", help="Try job name; default to current branch name")
2282 parser.add_option_group(group)
2283 options, args = parser.parse_args(args)
2284
2285 if args:
2286 parser.error('Unknown arguments: %s' % args)
2287
2288 cl = Changelist()
2289 if not cl.GetIssue():
2290 parser.error('Need to upload first')
2291
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002292 props = cl.GetIssueProperties()
2293 if props.get('private'):
2294 parser.error('Cannot use trybots with private issue')
2295
maruel@chromium.org15192402012-09-06 12:38:29 +00002296 if not options.name:
2297 options.name = cl.GetBranch()
2298
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002299 if options.bot and not options.master:
2300 parser.error('For manually specified bots please also specify the '
2301 'tryserver master, e.g. "-m tryserver.chromium".')
2302
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002303 def GetMasterMap():
2304 # Process --bot and --testfilter.
2305 if not options.bot:
2306 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002307
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002308 # Get try masters from PRESUBMIT.py files.
2309 masters = presubmit_support.DoGetTryMasters(
2310 change,
2311 change.LocalPaths(),
2312 settings.GetRoot(),
2313 None,
2314 None,
2315 options.verbose,
2316 sys.stdout)
2317 if masters:
2318 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002319
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002320 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2321 options.bot = presubmit_support.DoGetTrySlaves(
2322 change,
2323 change.LocalPaths(),
2324 settings.GetRoot(),
2325 None,
2326 None,
2327 options.verbose,
2328 sys.stdout)
2329 if not options.bot:
2330 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002331
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002332 builders_and_tests = {}
2333 # TODO(machenbach): The old style command-line options don't support
2334 # multiple try masters yet.
2335 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2336 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2337
2338 for bot in old_style:
2339 if ':' in bot:
2340 builder, tests = bot.split(':', 1)
2341 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2342 elif ',' in bot:
2343 parser.error('Specify one bot per --bot flag')
2344 else:
2345 builders_and_tests.setdefault(bot, []).append('defaulttests')
2346
2347 for bot, tests in new_style:
2348 builders_and_tests.setdefault(bot, []).extend(tests)
2349
2350 # Return a master map with one master to be backwards compatible. The
2351 # master name defaults to an empty string, which will cause the master
2352 # not to be set on rietveld (deprecated).
2353 return {options.master: builders_and_tests}
2354
2355 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002356
maruel@chromium.org15192402012-09-06 12:38:29 +00002357 if options.testfilter:
2358 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002359 masters = dict((master, dict(
2360 (b, forced_tests) for b, t in slaves.iteritems()
2361 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002362
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002363 for builders in masters.itervalues():
2364 if any('triggered' in b for b in builders):
2365 print >> sys.stderr, (
2366 'ERROR You are trying to send a job to a triggered bot. This type of'
2367 ' bot requires an\ninitial job from a parent (usually a builder). '
2368 'Instead send your job to the parent.\n'
2369 'Bot list: %s' % builders)
2370 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002371
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002372 patchset = cl.GetMostRecentPatchset()
2373 if patchset and patchset != cl.GetPatchset():
2374 print(
2375 '\nWARNING Mismatch between local config and server. Did a previous '
2376 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2377 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002378 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002379 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002380 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002381 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002382 except urllib2.HTTPError, e:
2383 if e.code == 404:
2384 print('404 from rietveld; '
2385 'did you mean to use "git try" instead of "git cl try"?')
2386 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002387 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002388
2389 for (master, builders) in masters.iteritems():
2390 if master:
2391 print 'Master: %s' % master
2392 length = max(len(builder) for builder in builders)
2393 for builder in sorted(builders):
2394 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002395 return 0
2396
2397
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002398@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002399def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002400 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002401 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002402 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002403 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002405 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002406 if args:
2407 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002408 branch = cl.GetBranch()
2409 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002410 cl = Changelist()
2411 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002412
2413 # Clear configured merge-base, if there is one.
2414 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002415 else:
2416 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002417 return 0
2418
2419
thestig@chromium.org00858c82013-12-02 23:08:03 +00002420def CMDweb(parser, args):
2421 """Opens the current CL in the web browser."""
2422 _, args = parser.parse_args(args)
2423 if args:
2424 parser.error('Unrecognized args: %s' % ' '.join(args))
2425
2426 issue_url = Changelist().GetIssueURL()
2427 if not issue_url:
2428 print >> sys.stderr, 'ERROR No issue to open'
2429 return 1
2430
2431 webbrowser.open(issue_url)
2432 return 0
2433
2434
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002435def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002436 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002437 _, args = parser.parse_args(args)
2438 if args:
2439 parser.error('Unrecognized args: %s' % ' '.join(args))
2440 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002441 props = cl.GetIssueProperties()
2442 if props.get('private'):
2443 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002444 cl.SetFlag('commit', '1')
2445 return 0
2446
2447
groby@chromium.org411034a2013-02-26 15:12:01 +00002448def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002449 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002450 _, args = parser.parse_args(args)
2451 if args:
2452 parser.error('Unrecognized args: %s' % ' '.join(args))
2453 cl = Changelist()
2454 # Ensure there actually is an issue to close.
2455 cl.GetDescription()
2456 cl.CloseIssue()
2457 return 0
2458
2459
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002460def CMDdiff(parser, args):
2461 """shows differences between local tree and last upload."""
2462 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002463 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002464 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002465 if not issue:
2466 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002467 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002468 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002469
2470 # Create a new branch based on the merge-base
2471 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2472 try:
2473 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002474 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002475 if rtn != 0:
2476 return rtn
2477
2478 # Switch back to starting brand and diff against the temporary
2479 # branch containing the latest rietveld patch.
2480 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2481 finally:
2482 RunGit(['checkout', '-q', branch])
2483 RunGit(['branch', '-D', TMP_BRANCH])
2484
2485 return 0
2486
2487
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002488def CMDowners(parser, args):
2489 """interactively find the owners for reviewing"""
2490 parser.add_option(
2491 '--no-color',
2492 action='store_true',
2493 help='Use this option to disable color output')
2494 options, args = parser.parse_args(args)
2495
2496 author = RunGit(['config', 'user.email']).strip() or None
2497
2498 cl = Changelist()
2499
2500 if args:
2501 if len(args) > 1:
2502 parser.error('Unknown args')
2503 base_branch = args[0]
2504 else:
2505 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002506 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002507
2508 change = cl.GetChange(base_branch, None)
2509 return owners_finder.OwnersFinder(
2510 [f.LocalPath() for f in
2511 cl.GetChange(base_branch, None).AffectedFiles()],
2512 change.RepositoryRoot(), author,
2513 fopen=file, os_path=os.path, glob=glob.glob,
2514 disable_color=options.no_color).run()
2515
2516
enne@chromium.org555cfe42014-01-29 18:21:39 +00002517@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002518def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002519 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002520 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002521 parser.add_option('--full', action='store_true',
2522 help='Reformat the full content of all touched files')
2523 parser.add_option('--dry-run', action='store_true',
2524 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002525 parser.add_option('--diff', action='store_true',
2526 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002527 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002528
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002529 # git diff generates paths against the root of the repository. Change
2530 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002531 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002532 if rel_base_path:
2533 os.chdir(rel_base_path)
2534
digit@chromium.org29e47272013-05-17 17:01:46 +00002535 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002536 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002537 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002538 # Only list the names of modified files.
2539 diff_cmd.append('--name-only')
2540 else:
2541 # Only generate context-less patches.
2542 diff_cmd.append('-U0')
2543
2544 # Grab the merge-base commit, i.e. the upstream commit of the current
2545 # branch when it was created or the last time it was rebased. This is
2546 # to cover the case where the user may have called "git fetch origin",
2547 # moving the origin branch to a newer commit, but hasn't rebased yet.
2548 upstream_commit = None
2549 cl = Changelist()
2550 upstream_branch = cl.GetUpstreamBranch()
2551 if upstream_branch:
2552 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2553 upstream_commit = upstream_commit.strip()
2554
2555 if not upstream_commit:
2556 DieWithError('Could not find base commit for this branch. '
2557 'Are you in detached state?')
2558
2559 diff_cmd.append(upstream_commit)
2560
2561 # Handle source file filtering.
2562 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002563 if args:
2564 for arg in args:
2565 if os.path.isdir(arg):
2566 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2567 elif os.path.isfile(arg):
2568 diff_cmd.append(arg)
2569 else:
2570 DieWithError('Argument "%s" is not a file or a directory' % arg)
2571 else:
2572 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002573 diff_output = RunGit(diff_cmd)
2574
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002575 top_dir = os.path.normpath(
2576 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2577
2578 # Locate the clang-format binary in the checkout
2579 try:
2580 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2581 except clang_format.NotFoundError, e:
2582 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002583
digit@chromium.org29e47272013-05-17 17:01:46 +00002584 if opts.full:
2585 # diff_output is a list of files to send to clang-format.
2586 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002587 if not files:
2588 print "Nothing to format."
2589 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002590 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002591 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002592 cmd.append('-i')
2593 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002594 if opts.diff:
2595 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002596 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002597 env = os.environ.copy()
2598 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002599 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002600 try:
2601 script = clang_format.FindClangFormatScriptInChromiumTree(
2602 'clang-format-diff.py')
2603 except clang_format.NotFoundError, e:
2604 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002605
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002606 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002607 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002608 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002609
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002610 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002611 if opts.diff:
2612 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002613 if opts.dry_run and len(stdout) > 0:
2614 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002615
2616 return 0
2617
2618
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002619class OptionParser(optparse.OptionParser):
2620 """Creates the option parse and add --verbose support."""
2621 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002622 optparse.OptionParser.__init__(
2623 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002624 self.add_option(
2625 '-v', '--verbose', action='count', default=0,
2626 help='Use 2 times for more debugging info')
2627
2628 def parse_args(self, args=None, values=None):
2629 options, args = optparse.OptionParser.parse_args(self, args, values)
2630 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2631 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2632 return options, args
2633
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002634
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002635def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002636 if sys.hexversion < 0x02060000:
2637 print >> sys.stderr, (
2638 '\nYour python version %s is unsupported, please upgrade.\n' %
2639 sys.version.split(' ', 1)[0])
2640 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002641
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002642 # Reload settings.
2643 global settings
2644 settings = Settings()
2645
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002646 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002647 dispatcher = subcommand.CommandDispatcher(__name__)
2648 try:
2649 return dispatcher.execute(OptionParser(), argv)
2650 except urllib2.HTTPError, e:
2651 if e.code != 500:
2652 raise
2653 DieWithError(
2654 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2655 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002656
2657
2658if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002659 # These affect sys.stdout so do it outside of main() to simplify mocks in
2660 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002661 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002662 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002663 sys.exit(main(sys.argv[1:]))