blob: f86a781765a4ab605459d63fbc8628a77f268dae [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:
330 # If you have any "svn-remote.*" config keys, we think you're using svn.
331 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000332 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333 return self.is_git_svn
334
335 def GetSVNBranch(self):
336 if self.svn_branch is None:
337 if not self.GetIsGitSvn():
338 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
339
340 # Try to figure out which remote branch we're based on.
341 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000342 # 1) iterate through our branch history and find the svn URL.
343 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000344
345 # regexp matching the git-svn line that contains the URL.
346 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
347
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000348 # We don't want to go through all of history, so read a line from the
349 # pipe at a time.
350 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000351 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000352 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
353 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000354 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000355 for line in proc.stdout:
356 match = git_svn_re.match(line)
357 if match:
358 url = match.group(1)
359 proc.stdout.close() # Cut pipe.
360 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000361
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 if url:
363 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
364 remotes = RunGit(['config', '--get-regexp',
365 r'^svn-remote\..*\.url']).splitlines()
366 for remote in remotes:
367 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000368 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000369 remote = match.group(1)
370 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000371 rewrite_root = RunGit(
372 ['config', 'svn-remote.%s.rewriteRoot' % remote],
373 error_ok=True).strip()
374 if rewrite_root:
375 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000376 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000377 ['config', 'svn-remote.%s.fetch' % remote],
378 error_ok=True).strip()
379 if fetch_spec:
380 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
381 if self.svn_branch:
382 break
383 branch_spec = RunGit(
384 ['config', 'svn-remote.%s.branches' % remote],
385 error_ok=True).strip()
386 if branch_spec:
387 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
388 if self.svn_branch:
389 break
390 tag_spec = RunGit(
391 ['config', 'svn-remote.%s.tags' % remote],
392 error_ok=True).strip()
393 if tag_spec:
394 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
395 if self.svn_branch:
396 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000397
398 if not self.svn_branch:
399 DieWithError('Can\'t guess svn branch -- try specifying it on the '
400 'command line')
401
402 return self.svn_branch
403
404 def GetTreeStatusUrl(self, error_ok=False):
405 if not self.tree_status_url:
406 error_message = ('You must configure your tree status URL by running '
407 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000408 self.tree_status_url = self._GetRietveldConfig(
409 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000410 return self.tree_status_url
411
412 def GetViewVCUrl(self):
413 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000414 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 return self.viewvc_url
416
rmistry@google.com90752582014-01-14 21:04:50 +0000417 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000418 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000419
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000420 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000421 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000422
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000423 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000424 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000425
ukai@chromium.orge8077812012-02-03 03:41:46 +0000426 def GetIsGerrit(self):
427 """Return true if this repo is assosiated with gerrit code review system."""
428 if self.is_gerrit is None:
429 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
430 return self.is_gerrit
431
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000432 def GetGitEditor(self):
433 """Return the editor specified in the git config, or None if none is."""
434 if self.git_editor is None:
435 self.git_editor = self._GetConfig('core.editor', error_ok=True)
436 return self.git_editor or None
437
thestig@chromium.org44202a22014-03-11 19:22:18 +0000438 def GetLintRegex(self):
439 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
440 DEFAULT_LINT_REGEX)
441
442 def GetLintIgnoreRegex(self):
443 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
444 DEFAULT_LINT_IGNORE_REGEX)
445
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000446 def GetProject(self):
447 if not self.project:
448 self.project = self._GetRietveldConfig('project', error_ok=True)
449 return self.project
450
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000451 def _GetRietveldConfig(self, param, **kwargs):
452 return self._GetConfig('rietveld.' + param, **kwargs)
453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000454 def _GetConfig(self, param, **kwargs):
455 self.LazyUpdateIfNeeded()
456 return RunGit(['config', param], **kwargs).strip()
457
458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459def ShortBranchName(branch):
460 """Convert a name like 'refs/heads/foo' to just 'foo'."""
461 return branch.replace('refs/heads/', '')
462
463
464class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000465 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000467 global settings
468 if not settings:
469 # Happens when git_cl.py is used as a utility library.
470 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 settings.GetDefaultServerUrl()
472 self.branchref = branchref
473 if self.branchref:
474 self.branch = ShortBranchName(self.branchref)
475 else:
476 self.branch = None
477 self.rietveld_server = None
478 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000479 self.lookedup_issue = False
480 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 self.has_description = False
482 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000483 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000485 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000486 self.cc = None
487 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000488 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000489 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000490
491 def GetCCList(self):
492 """Return the users cc'd on this CL.
493
494 Return is a string suitable for passing to gcl with the --cc flag.
495 """
496 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000497 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000498 more_cc = ','.join(self.watchers)
499 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
500 return self.cc
501
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000502 def GetCCListWithoutDefault(self):
503 """Return the users cc'd on this CL excluding default ones."""
504 if self.cc is None:
505 self.cc = ','.join(self.watchers)
506 return self.cc
507
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000508 def SetWatchers(self, watchers):
509 """Set the list of email addresses that should be cc'd based on the changed
510 files in this CL.
511 """
512 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000513
514 def GetBranch(self):
515 """Returns the short branch name, e.g. 'master'."""
516 if not self.branch:
517 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
518 self.branch = ShortBranchName(self.branchref)
519 return self.branch
520
521 def GetBranchRef(self):
522 """Returns the full branch name, e.g. 'refs/heads/master'."""
523 self.GetBranch() # Poke the lazy loader.
524 return self.branchref
525
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000526 @staticmethod
527 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000528 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000529 e.g. 'origin', 'refs/heads/master'
530 """
531 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000532 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
533 error_ok=True).strip()
534 if upstream_branch:
535 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
536 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000537 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
538 error_ok=True).strip()
539 if upstream_branch:
540 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000541 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000542 # Fall back on trying a git-svn upstream branch.
543 if settings.GetIsGitSvn():
544 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000545 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000546 # Else, try to guess the origin remote.
547 remote_branches = RunGit(['branch', '-r']).split()
548 if 'origin/master' in remote_branches:
549 # Fall back on origin/master if it exits.
550 remote = 'origin'
551 upstream_branch = 'refs/heads/master'
552 elif 'origin/trunk' in remote_branches:
553 # Fall back on origin/trunk if it exists. Generally a shared
554 # git-svn clone
555 remote = 'origin'
556 upstream_branch = 'refs/heads/trunk'
557 else:
558 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000559Either pass complete "git diff"-style arguments, like
560 git cl upload origin/master
561or verify this branch is set up to track another (via the --track argument to
562"git checkout -b ...").""")
563
564 return remote, upstream_branch
565
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000566 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000567 return git_common.get_or_create_merge_base(self.GetBranch(),
568 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000569
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 def GetUpstreamBranch(self):
571 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000572 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573 if remote is not '.':
574 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
575 self.upstream_branch = upstream_branch
576 return self.upstream_branch
577
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000578 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000579 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000580 remote, branch = None, self.GetBranch()
581 seen_branches = set()
582 while branch not in seen_branches:
583 seen_branches.add(branch)
584 remote, branch = self.FetchUpstreamTuple(branch)
585 branch = ShortBranchName(branch)
586 if remote != '.' or branch.startswith('refs/remotes'):
587 break
588 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000589 remotes = RunGit(['remote'], error_ok=True).split()
590 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000591 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000592 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000593 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000594 logging.warning('Could not determine which remote this change is '
595 'associated with, so defaulting to "%s". This may '
596 'not be what you want. You may prevent this message '
597 'by running "git svn info" as documented here: %s',
598 self._remote,
599 GIT_INSTRUCTIONS_URL)
600 else:
601 logging.warn('Could not determine which remote this change is '
602 'associated with. You may prevent this message by '
603 'running "git svn info" as documented here: %s',
604 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000605 branch = 'HEAD'
606 if branch.startswith('refs/remotes'):
607 self._remote = (remote, branch)
608 else:
609 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000610 return self._remote
611
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000612 def GitSanityChecks(self, upstream_git_obj):
613 """Checks git repo status and ensures diff is from local commits."""
614
615 # Verify the commit we're diffing against is in our current branch.
616 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
617 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
618 if upstream_sha != common_ancestor:
619 print >> sys.stderr, (
620 'ERROR: %s is not in the current branch. You may need to rebase '
621 'your tracking branch' % upstream_sha)
622 return False
623
624 # List the commits inside the diff, and verify they are all local.
625 commits_in_diff = RunGit(
626 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
627 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
628 remote_branch = remote_branch.strip()
629 if code != 0:
630 _, remote_branch = self.GetRemoteBranch()
631
632 commits_in_remote = RunGit(
633 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
634
635 common_commits = set(commits_in_diff) & set(commits_in_remote)
636 if common_commits:
637 print >> sys.stderr, (
638 'ERROR: Your diff contains %d commits already in %s.\n'
639 'Run "git log --oneline %s..HEAD" to get a list of commits in '
640 'the diff. If you are using a custom git flow, you can override'
641 ' the reference used for this check with "git config '
642 'gitcl.remotebranch <git-ref>".' % (
643 len(common_commits), remote_branch, upstream_git_obj))
644 return False
645 return True
646
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000647 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000648 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000649
650 Returns None if it is not set.
651 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000652 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
653 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000654
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 def GetRemoteUrl(self):
656 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
657
658 Returns None if there is no remote.
659 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000660 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000661 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
662
663 # If URL is pointing to a local directory, it is probably a git cache.
664 if os.path.isdir(url):
665 url = RunGit(['config', 'remote.%s.url' % remote],
666 error_ok=True,
667 cwd=url).strip()
668 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669
670 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000671 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000674 self.issue = int(issue) or None if issue else None
675 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 return self.issue
677
678 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000679 if not self.rietveld_server:
680 # If we're on a branch then get the server potentially associated
681 # with that branch.
682 if self.GetIssue():
683 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
684 ['config', self._RietveldServer()], error_ok=True).strip())
685 if not self.rietveld_server:
686 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687 return self.rietveld_server
688
689 def GetIssueURL(self):
690 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000691 if not self.GetIssue():
692 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000693 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
694
695 def GetDescription(self, pretty=False):
696 if not self.has_description:
697 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000698 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000699 try:
700 self.description = self.RpcServer().get_description(issue).strip()
701 except urllib2.HTTPError, e:
702 if e.code == 404:
703 DieWithError(
704 ('\nWhile fetching the description for issue %d, received a '
705 '404 (not found)\n'
706 'error. It is likely that you deleted this '
707 'issue on the server. If this is the\n'
708 'case, please run\n\n'
709 ' git cl issue 0\n\n'
710 'to clear the association with the deleted issue. Then run '
711 'this command again.') % issue)
712 else:
713 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000714 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 self.has_description = True
716 if pretty:
717 wrapper = textwrap.TextWrapper()
718 wrapper.initial_indent = wrapper.subsequent_indent = ' '
719 return wrapper.fill(self.description)
720 return self.description
721
722 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000723 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000724 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 patchset = RunGit(['config', self._PatchsetSetting()],
726 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000727 self.patchset = int(patchset) or None if patchset else None
728 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 return self.patchset
730
731 def SetPatchset(self, patchset):
732 """Set this branch's patchset. If patchset=0, clears the patchset."""
733 if patchset:
734 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000735 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 else:
737 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000738 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000739 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000741 def GetMostRecentPatchset(self):
742 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000743
744 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000745 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000746 '/download/issue%s_%s.diff' % (issue, patchset))
747
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000748 def GetIssueProperties(self):
749 if self._props is None:
750 issue = self.GetIssue()
751 if not issue:
752 self._props = {}
753 else:
754 self._props = self.RpcServer().get_issue_properties(issue, True)
755 return self._props
756
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000757 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000758 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 def SetIssue(self, issue):
761 """Set this branch's issue. If issue=0, clears the issue."""
762 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000763 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 RunGit(['config', self._IssueSetting(), str(issue)])
765 if self.rietveld_server:
766 RunGit(['config', self._RietveldServer(), self.rietveld_server])
767 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000768 current_issue = self.GetIssue()
769 if current_issue:
770 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000771 self.issue = None
772 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000774 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000775 if not self.GitSanityChecks(upstream_branch):
776 DieWithError('\nGit sanity check failure')
777
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000778 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000779 if not root:
780 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000781 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000782
783 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000784 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000785 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000786 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000787 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000788 except subprocess2.CalledProcessError:
789 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000790 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000791 'This branch probably doesn\'t exist anymore. To reset the\n'
792 'tracking branch, please run\n'
793 ' git branch --set-upstream %s trunk\n'
794 'replacing trunk with origin/master or the relevant branch') %
795 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000796
maruel@chromium.org52424302012-08-29 15:14:30 +0000797 issue = self.GetIssue()
798 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000799 if issue:
800 description = self.GetDescription()
801 else:
802 # If the change was never uploaded, use the log messages of all commits
803 # up to the branch point, as git cl upload will prefill the description
804 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
806 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000807
808 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000809 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000810 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000811 name,
812 description,
813 absroot,
814 files,
815 issue,
816 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000817 author,
818 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000819
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000820 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000821 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000822
823 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000824 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000825 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000826 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000827 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000828 except presubmit_support.PresubmitFailure, e:
829 DieWithError(
830 ('%s\nMaybe your depot_tools is out of date?\n'
831 'If all fails, contact maruel@') % e)
832
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000833 def UpdateDescription(self, description):
834 self.description = description
835 return self.RpcServer().update_description(
836 self.GetIssue(), self.description)
837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000839 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000840 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000842 def SetFlag(self, flag, value):
843 """Patchset must match."""
844 if not self.GetPatchset():
845 DieWithError('The patchset needs to match. Send another patchset.')
846 try:
847 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000848 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000849 except urllib2.HTTPError, e:
850 if e.code == 404:
851 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
852 if e.code == 403:
853 DieWithError(
854 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
855 'match?') % (self.GetIssue(), self.GetPatchset()))
856 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000858 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 """Returns an upload.RpcServer() to access this review's rietveld instance.
860 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000861 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000862 self._rpc_server = rietveld.CachingRietveld(
863 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000864 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865
866 def _IssueSetting(self):
867 """Return the git setting that stores this change's issue."""
868 return 'branch.%s.rietveldissue' % self.GetBranch()
869
870 def _PatchsetSetting(self):
871 """Return the git setting that stores this change's most recent patchset."""
872 return 'branch.%s.rietveldpatchset' % self.GetBranch()
873
874 def _RietveldServer(self):
875 """Returns the git setting that stores this change's rietveld server."""
876 return 'branch.%s.rietveldserver' % self.GetBranch()
877
878
879def GetCodereviewSettingsInteractively():
880 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000881 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 server = settings.GetDefaultServerUrl(error_ok=True)
883 prompt = 'Rietveld server (host[:port])'
884 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000885 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886 if not server and not newserver:
887 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000888 if newserver:
889 newserver = gclient_utils.UpgradeToHttps(newserver)
890 if newserver != server:
891 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000893 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894 prompt = caption
895 if initial:
896 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000897 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 if new_val == 'x':
899 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000900 elif new_val:
901 if is_url:
902 new_val = gclient_utils.UpgradeToHttps(new_val)
903 if new_val != initial:
904 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000906 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000907 SetProperty(settings.GetDefaultPrivateFlag(),
908 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000910 'tree-status-url', False)
911 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000912 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913
914 # TODO: configure a default branch to diff against, rather than this
915 # svn-based hackery.
916
917
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000918class ChangeDescription(object):
919 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000920 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000921 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000922
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000923 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000924 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000925
agable@chromium.org42c20792013-09-12 17:34:49 +0000926 @property # www.logilab.org/ticket/89786
927 def description(self): # pylint: disable=E0202
928 return '\n'.join(self._description_lines)
929
930 def set_description(self, desc):
931 if isinstance(desc, basestring):
932 lines = desc.splitlines()
933 else:
934 lines = [line.rstrip() for line in desc]
935 while lines and not lines[0]:
936 lines.pop(0)
937 while lines and not lines[-1]:
938 lines.pop(-1)
939 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000940
941 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000943 assert isinstance(reviewers, list), reviewers
944 if not reviewers:
945 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000946 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000947
agable@chromium.org42c20792013-09-12 17:34:49 +0000948 # Get the set of R= and TBR= lines and remove them from the desciption.
949 regexp = re.compile(self.R_LINE)
950 matches = [regexp.match(line) for line in self._description_lines]
951 new_desc = [l for i, l in enumerate(self._description_lines)
952 if not matches[i]]
953 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000954
agable@chromium.org42c20792013-09-12 17:34:49 +0000955 # Construct new unified R= and TBR= lines.
956 r_names = []
957 tbr_names = []
958 for match in matches:
959 if not match:
960 continue
961 people = cleanup_list([match.group(2).strip()])
962 if match.group(1) == 'TBR':
963 tbr_names.extend(people)
964 else:
965 r_names.extend(people)
966 for name in r_names:
967 if name not in reviewers:
968 reviewers.append(name)
969 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
970 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
971
972 # Put the new lines in the description where the old first R= line was.
973 line_loc = next((i for i, match in enumerate(matches) if match), -1)
974 if 0 <= line_loc < len(self._description_lines):
975 if new_tbr_line:
976 self._description_lines.insert(line_loc, new_tbr_line)
977 if new_r_line:
978 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000979 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000980 if new_r_line:
981 self.append_footer(new_r_line)
982 if new_tbr_line:
983 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000984
985 def prompt(self):
986 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000987 self.set_description([
988 '# Enter a description of the change.',
989 '# This will be displayed on the codereview site.',
990 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000991 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000992 '--------------------',
993 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000994
agable@chromium.org42c20792013-09-12 17:34:49 +0000995 regexp = re.compile(self.BUG_LINE)
996 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000997 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000998 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000999 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001000 if not content:
1001 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001002 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001003
1004 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001005 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1006 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001007 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001008 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001009
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001010 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001011 if self._description_lines:
1012 # Add an empty line if either the last line or the new line isn't a tag.
1013 last_line = self._description_lines[-1]
1014 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1015 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1016 self._description_lines.append('')
1017 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001018
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001019 def get_reviewers(self):
1020 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001021 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1022 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001023 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001024
1025
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001026def get_approving_reviewers(props):
1027 """Retrieves the reviewers that approved a CL from the issue properties with
1028 messages.
1029
1030 Note that the list may contain reviewers that are not committer, thus are not
1031 considered by the CQ.
1032 """
1033 return sorted(
1034 set(
1035 message['sender']
1036 for message in props['messages']
1037 if message['approval'] and message['sender'] in props['reviewers']
1038 )
1039 )
1040
1041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042def FindCodereviewSettingsFile(filename='codereview.settings'):
1043 """Finds the given file starting in the cwd and going up.
1044
1045 Only looks up to the top of the repository unless an
1046 'inherit-review-settings-ok' file exists in the root of the repository.
1047 """
1048 inherit_ok_file = 'inherit-review-settings-ok'
1049 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001050 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1052 root = '/'
1053 while True:
1054 if filename in os.listdir(cwd):
1055 if os.path.isfile(os.path.join(cwd, filename)):
1056 return open(os.path.join(cwd, filename))
1057 if cwd == root:
1058 break
1059 cwd = os.path.dirname(cwd)
1060
1061
1062def LoadCodereviewSettingsFromFile(fileobj):
1063 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001064 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066 def SetProperty(name, setting, unset_error_ok=False):
1067 fullname = 'rietveld.' + name
1068 if setting in keyvals:
1069 RunGit(['config', fullname, keyvals[setting]])
1070 else:
1071 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1072
1073 SetProperty('server', 'CODE_REVIEW_SERVER')
1074 # Only server setting is required. Other settings can be absent.
1075 # In that case, we ignore errors raised during option deletion attempt.
1076 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001077 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1079 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001080 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001081 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1082 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001083 SetProperty('project', 'PROJECT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001085 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001086 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1089 #should be of the form
1090 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1091 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1092 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1093 keyvals['ORIGIN_URL_CONFIG']])
1094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001096def urlretrieve(source, destination):
1097 """urllib is broken for SSL connections via a proxy therefore we
1098 can't use urllib.urlretrieve()."""
1099 with open(destination, 'w') as f:
1100 f.write(urllib2.urlopen(source).read())
1101
1102
ukai@chromium.org712d6102013-11-27 00:52:58 +00001103def hasSheBang(fname):
1104 """Checks fname is a #! script."""
1105 with open(fname) as f:
1106 return f.read(2).startswith('#!')
1107
1108
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001109def DownloadHooks(force):
1110 """downloads hooks
1111
1112 Args:
1113 force: True to update hooks. False to install hooks if not present.
1114 """
1115 if not settings.GetIsGerrit():
1116 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001117 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001118 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1119 if not os.access(dst, os.X_OK):
1120 if os.path.exists(dst):
1121 if not force:
1122 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001123 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001124 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001125 if not hasSheBang(dst):
1126 DieWithError('Not a script: %s\n'
1127 'You need to download from\n%s\n'
1128 'into .git/hooks/commit-msg and '
1129 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001130 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1131 except Exception:
1132 if os.path.exists(dst):
1133 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001134 DieWithError('\nFailed to download hooks.\n'
1135 'You need to download from\n%s\n'
1136 'into .git/hooks/commit-msg and '
1137 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001138
1139
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001140@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001142 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001144 parser.add_option('--activate-update', action='store_true',
1145 help='activate auto-updating [rietveld] section in '
1146 '.git/config')
1147 parser.add_option('--deactivate-update', action='store_true',
1148 help='deactivate auto-updating [rietveld] section in '
1149 '.git/config')
1150 options, args = parser.parse_args(args)
1151
1152 if options.deactivate_update:
1153 RunGit(['config', 'rietveld.autoupdate', 'false'])
1154 return
1155
1156 if options.activate_update:
1157 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1158 return
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 if len(args) == 0:
1161 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001162 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 return 0
1164
1165 url = args[0]
1166 if not url.endswith('codereview.settings'):
1167 url = os.path.join(url, 'codereview.settings')
1168
1169 # Load code review settings and download hooks (if available).
1170 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001171 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 return 0
1173
1174
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001175def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001176 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001177 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1178 branch = ShortBranchName(branchref)
1179 _, args = parser.parse_args(args)
1180 if not args:
1181 print("Current base-url:")
1182 return RunGit(['config', 'branch.%s.base-url' % branch],
1183 error_ok=False).strip()
1184 else:
1185 print("Setting base-url to %s" % args[0])
1186 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1187 error_ok=False).strip()
1188
1189
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001191 """Show status of changelists.
1192
1193 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001194 - Red not sent for review or broken
1195 - Blue waiting for review
1196 - Yellow waiting for you to reply to review
1197 - Green LGTM'ed
1198 - Magenta in the commit queue
1199 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001200
1201 Also see 'git cl comments'.
1202 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 parser.add_option('--field',
1204 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 parser.add_option('-f', '--fast', action='store_true',
1206 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001208 if args:
1209 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001212 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 if options.field.startswith('desc'):
1214 print cl.GetDescription()
1215 elif options.field == 'id':
1216 issueid = cl.GetIssue()
1217 if issueid:
1218 print issueid
1219 elif options.field == 'patch':
1220 patchset = cl.GetPatchset()
1221 if patchset:
1222 print patchset
1223 elif options.field == 'url':
1224 url = cl.GetIssueURL()
1225 if url:
1226 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001227 return 0
1228
1229 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1230 if not branches:
1231 print('No local branch found.')
1232 return 0
1233
1234 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001235 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001236 alignment = max(5, max(len(b) for b in branches))
1237 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001238 # Adhoc thread pool to request data concurrently.
1239 output = Queue.Queue()
1240
1241 # Silence upload.py otherwise it becomes unweldly.
1242 upload.verbosity = 0
1243
1244 if not options.fast:
1245 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001246 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001247 c = Changelist(branchref=b)
1248 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001249 props = {}
1250 r = None
1251 if i:
1252 try:
1253 props = c.GetIssueProperties()
1254 r = c.GetApprovingReviewers() if i else None
1255 except urllib2.HTTPError:
1256 # The issue probably doesn't exist anymore.
1257 i += ' (broken)'
1258
1259 msgs = props.get('messages') or []
1260
1261 if not i:
1262 color = Fore.WHITE
1263 elif props.get('closed'):
1264 # Issue is closed.
1265 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001266 elif props.get('commit'):
1267 # Issue is in the commit queue.
1268 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001269 elif r:
1270 # Was LGTM'ed.
1271 color = Fore.GREEN
1272 elif not msgs:
1273 # No message was sent.
1274 color = Fore.RED
1275 elif msgs[-1]['sender'] != props.get('owner_email'):
1276 color = Fore.YELLOW
1277 else:
1278 color = Fore.BLUE
1279 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001280
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001281 # Process one branch synchronously to work through authentication, then
1282 # spawn threads to process all the other branches in parallel.
1283 if branches:
1284 fetch(branches[0])
1285 threads = [
1286 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001287 for t in threads:
1288 t.daemon = True
1289 t.start()
1290 else:
1291 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1292 for b in branches:
1293 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001294 url = c.GetIssueURL()
1295 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001296
1297 tmp = {}
1298 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001299 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001300 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001301 b, i, color = output.get()
1302 tmp[b] = (i, color)
1303 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001304 reset = Fore.RESET
1305 if not sys.stdout.isatty():
1306 color = ''
1307 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001308 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001309 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001310
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001311 cl = Changelist()
1312 print
1313 print 'Current branch:',
1314 if not cl.GetIssue():
1315 print 'no issue assigned.'
1316 return 0
1317 print cl.GetBranch()
1318 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1319 print 'Issue description:'
1320 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 return 0
1322
1323
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001324def colorize_CMDstatus_doc():
1325 """To be called once in main() to add colors to git cl status help."""
1326 colors = [i for i in dir(Fore) if i[0].isupper()]
1327
1328 def colorize_line(line):
1329 for color in colors:
1330 if color in line.upper():
1331 # Extract whitespaces first and the leading '-'.
1332 indent = len(line) - len(line.lstrip(' ')) + 1
1333 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1334 return line
1335
1336 lines = CMDstatus.__doc__.splitlines()
1337 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1338
1339
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001340@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001342 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343
1344 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001345 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001346 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347
1348 cl = Changelist()
1349 if len(args) > 0:
1350 try:
1351 issue = int(args[0])
1352 except ValueError:
1353 DieWithError('Pass a number to set the issue or none to list it.\n'
1354 'Maybe you want to run git cl status?')
1355 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001356 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 return 0
1358
1359
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001360def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001361 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001362 (_, args) = parser.parse_args(args)
1363 if args:
1364 parser.error('Unsupported argument: %s' % args)
1365
1366 cl = Changelist()
1367 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001368 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001369 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001370 if message['disapproval']:
1371 color = Fore.RED
1372 elif message['approval']:
1373 color = Fore.GREEN
1374 elif message['sender'] == data['owner_email']:
1375 color = Fore.MAGENTA
1376 else:
1377 color = Fore.BLUE
1378 print '\n%s%s %s%s' % (
1379 color, message['date'].split('.', 1)[0], message['sender'],
1380 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001381 if message['text'].strip():
1382 print '\n'.join(' ' + l for l in message['text'].splitlines())
1383 return 0
1384
1385
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001386def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001387 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001388 cl = Changelist()
1389 if not cl.GetIssue():
1390 DieWithError('This branch has no associated changelist.')
1391 description = ChangeDescription(cl.GetDescription())
1392 description.prompt()
1393 cl.UpdateDescription(description.description)
1394 return 0
1395
1396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397def CreateDescriptionFromLog(args):
1398 """Pulls out the commit log to use as a base for the CL description."""
1399 log_args = []
1400 if len(args) == 1 and not args[0].endswith('.'):
1401 log_args = [args[0] + '..']
1402 elif len(args) == 1 and args[0].endswith('...'):
1403 log_args = [args[0][:-1]]
1404 elif len(args) == 2:
1405 log_args = [args[0] + '..' + args[1]]
1406 else:
1407 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001408 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
1410
thestig@chromium.org44202a22014-03-11 19:22:18 +00001411def CMDlint(parser, args):
1412 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001413 parser.add_option('--filter', action='append', metavar='-x,+y',
1414 help='Comma-separated list of cpplint\'s category-filters')
1415 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001416
1417 # Access to a protected member _XX of a client class
1418 # pylint: disable=W0212
1419 try:
1420 import cpplint
1421 import cpplint_chromium
1422 except ImportError:
1423 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1424 return 1
1425
1426 # Change the current working directory before calling lint so that it
1427 # shows the correct base.
1428 previous_cwd = os.getcwd()
1429 os.chdir(settings.GetRoot())
1430 try:
1431 cl = Changelist()
1432 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1433 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001434 if not files:
1435 print "Cannot lint an empty CL"
1436 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001437
1438 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001439 command = args + files
1440 if options.filter:
1441 command = ['--filter=' + ','.join(options.filter)] + command
1442 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001443
1444 white_regex = re.compile(settings.GetLintRegex())
1445 black_regex = re.compile(settings.GetLintIgnoreRegex())
1446 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1447 for filename in filenames:
1448 if white_regex.match(filename):
1449 if black_regex.match(filename):
1450 print "Ignoring file %s" % filename
1451 else:
1452 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1453 extra_check_functions)
1454 else:
1455 print "Skipping file %s" % filename
1456 finally:
1457 os.chdir(previous_cwd)
1458 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1459 if cpplint._cpplint_state.error_count != 0:
1460 return 1
1461 return 0
1462
1463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001465 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001466 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001468 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001469 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 (options, args) = parser.parse_args(args)
1471
ukai@chromium.org259e4682012-10-25 07:36:33 +00001472 if not options.force and is_dirty_git_tree('presubmit'):
1473 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 return 1
1475
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001476 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 if args:
1478 base_branch = args[0]
1479 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001480 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001481 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001483 cl.RunHook(
1484 committing=not options.upload,
1485 may_prompt=False,
1486 verbose=options.verbose,
1487 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001488 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489
1490
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001491def AddChangeIdToCommitMessage(options, args):
1492 """Re-commits using the current message, assumes the commit hook is in
1493 place.
1494 """
1495 log_desc = options.message or CreateDescriptionFromLog(args)
1496 git_command = ['commit', '--amend', '-m', log_desc]
1497 RunGit(git_command)
1498 new_log_desc = CreateDescriptionFromLog(args)
1499 if CHANGE_ID in new_log_desc:
1500 print 'git-cl: Added Change-Id to commit message.'
1501 else:
1502 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1503
1504
ukai@chromium.orge8077812012-02-03 03:41:46 +00001505def GerritUpload(options, args, cl):
1506 """upload the current branch to gerrit."""
1507 # We assume the remote called "origin" is the one we want.
1508 # It is probably not worthwhile to support different workflows.
1509 remote = 'origin'
1510 branch = 'master'
1511 if options.target_branch:
1512 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001514 change_desc = ChangeDescription(
1515 options.message or CreateDescriptionFromLog(args))
1516 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001517 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001519 if CHANGE_ID not in change_desc.description:
1520 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001521
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001522 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001523 if len(commits) > 1:
1524 print('WARNING: This will upload %d commits. Run the following command '
1525 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001526 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001527 print('You can also use `git squash-branch` to squash these into a single'
1528 'commit.')
1529 ask_for_data('About to upload; enter to confirm.')
1530
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001531 if options.reviewers:
1532 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533
ukai@chromium.orge8077812012-02-03 03:41:46 +00001534 receive_options = []
1535 cc = cl.GetCCList().split(',')
1536 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001537 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001538 cc = filter(None, cc)
1539 if cc:
1540 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001541 if change_desc.get_reviewers():
1542 receive_options.extend(
1543 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544
ukai@chromium.orge8077812012-02-03 03:41:46 +00001545 git_command = ['push']
1546 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001547 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001548 ' '.join(receive_options))
1549 git_command += [remote, 'HEAD:refs/for/' + branch]
1550 RunGit(git_command)
1551 # TODO(ukai): parse Change-Id: and set issue number?
1552 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001553
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554
ukai@chromium.orge8077812012-02-03 03:41:46 +00001555def RietveldUpload(options, args, cl):
1556 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1558 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559 if options.emulate_svn_auto_props:
1560 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001561
1562 change_desc = None
1563
pgervais@chromium.org91141372014-01-09 23:27:20 +00001564 if options.email is not None:
1565 upload_args.extend(['--email', options.email])
1566
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001568 if options.title:
1569 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001570 if options.message:
1571 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001572 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001573 print ("This branch is associated with issue %s. "
1574 "Adding patch to that issue." % cl.GetIssue())
1575 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001576 if options.title:
1577 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001578 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001579 change_desc = ChangeDescription(message)
1580 if options.reviewers:
1581 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001582 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001583 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001584
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001585 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001586 print "Description is empty; aborting."
1587 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001588
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001589 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001590 if change_desc.get_reviewers():
1591 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001592 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001593 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001594 DieWithError("Must specify reviewers to send email.")
1595 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001596
1597 # We check this before applying rietveld.private assuming that in
1598 # rietveld.cc only addresses which we can send private CLs to are listed
1599 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1600 # --private is specified explicitly on the command line.
1601 if options.private:
1602 logging.warn('rietveld.cc is ignored since private flag is specified. '
1603 'You need to review and add them manually if necessary.')
1604 cc = cl.GetCCListWithoutDefault()
1605 else:
1606 cc = cl.GetCCList()
1607 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001608 if cc:
1609 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001611 if options.private or settings.GetDefaultPrivateFlag() == "True":
1612 upload_args.append('--private')
1613
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001614 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001615 if not options.find_copies:
1616 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001617
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001618 # Include the upstream repo's URL in the change -- this is useful for
1619 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001620 remote_url = cl.GetGitBaseUrlFromConfig()
1621 if not remote_url:
1622 if settings.GetIsGitSvn():
1623 # URL is dependent on the current directory.
1624 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1625 if data:
1626 keys = dict(line.split(': ', 1) for line in data.splitlines()
1627 if ': ' in line)
1628 remote_url = keys.get('URL', None)
1629 else:
1630 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1631 remote_url = (cl.GetRemoteUrl() + '@'
1632 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001633 if remote_url:
1634 upload_args.extend(['--base_url', remote_url])
1635
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001636 project = settings.GetProject()
1637 if project:
1638 upload_args.extend(['--project', project])
1639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001641 upload_args = ['upload'] + upload_args + args
1642 logging.info('upload.RealMain(%s)', upload_args)
1643 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001644 issue = int(issue)
1645 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001646 except KeyboardInterrupt:
1647 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001648 except:
1649 # If we got an exception after the user typed a description for their
1650 # change, back up the description before re-raising.
1651 if change_desc:
1652 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1653 print '\nGot exception while uploading -- saving description to %s\n' \
1654 % backup_path
1655 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001656 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001657 backup_file.close()
1658 raise
1659
1660 if not cl.GetIssue():
1661 cl.SetIssue(issue)
1662 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001663
1664 if options.use_commit_queue:
1665 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001666 return 0
1667
1668
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001669def cleanup_list(l):
1670 """Fixes a list so that comma separated items are put as individual items.
1671
1672 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1673 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1674 """
1675 items = sum((i.split(',') for i in l), [])
1676 stripped_items = (i.strip() for i in items)
1677 return sorted(filter(None, stripped_items))
1678
1679
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001680@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001681def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001682 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001683 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1684 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001685 parser.add_option('--bypass-watchlists', action='store_true',
1686 dest='bypass_watchlists',
1687 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001688 parser.add_option('-f', action='store_true', dest='force',
1689 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001690 parser.add_option('-m', dest='message', help='message for patchset')
1691 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001692 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001693 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001694 help='reviewer email addresses')
1695 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001696 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001697 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001698 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001699 help='send email to reviewer immediately')
1700 parser.add_option("--emulate_svn_auto_props", action="store_true",
1701 dest="emulate_svn_auto_props",
1702 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001703 parser.add_option('-c', '--use-commit-queue', action='store_true',
1704 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001705 parser.add_option('--private', action='store_true',
1706 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001707 parser.add_option('--target_branch',
1708 help='When uploading to gerrit, remote branch to '
1709 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001710 parser.add_option('--email', default=None,
1711 help='email address to use to connect to Rietveld')
1712
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001713 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001714 (options, args) = parser.parse_args(args)
1715
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001716 if options.target_branch and not settings.GetIsGerrit():
1717 parser.error('Use --target_branch for non gerrit repository.')
1718
ukai@chromium.org259e4682012-10-25 07:36:33 +00001719 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001720 return 1
1721
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001722 options.reviewers = cleanup_list(options.reviewers)
1723 options.cc = cleanup_list(options.cc)
1724
ukai@chromium.orge8077812012-02-03 03:41:46 +00001725 cl = Changelist()
1726 if args:
1727 # TODO(ukai): is it ok for gerrit case?
1728 base_branch = args[0]
1729 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001730 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001731 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001732 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001733
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001734 # Apply watchlists on upload.
1735 change = cl.GetChange(base_branch, None)
1736 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1737 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001738 if not options.bypass_watchlists:
1739 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001740
ukai@chromium.orge8077812012-02-03 03:41:46 +00001741 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001742 if options.reviewers:
1743 # Set the reviewer list now so that presubmit checks can access it.
1744 change_description = ChangeDescription(change.FullDescriptionText())
1745 change_description.update_reviewers(options.reviewers)
1746 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001747 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001748 may_prompt=not options.force,
1749 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001750 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001751 if not hook_results.should_continue():
1752 return 1
1753 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001754 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001755
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001756 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001757 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001758 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001759 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001760 print ('The last upload made from this repository was patchset #%d but '
1761 'the most recent patchset on the server is #%d.'
1762 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001763 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1764 'from another machine or branch the patch you\'re uploading now '
1765 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001766 ask_for_data('About to upload; enter to confirm.')
1767
iannucci@chromium.org79540052012-10-19 23:15:26 +00001768 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001769 if settings.GetIsGerrit():
1770 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001771 ret = RietveldUpload(options, args, cl)
1772 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001773 git_set_branch_value('last-upload-hash',
1774 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001775
1776 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001777
1778
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001779def IsSubmoduleMergeCommit(ref):
1780 # When submodules are added to the repo, we expect there to be a single
1781 # non-git-svn merge commit at remote HEAD with a signature comment.
1782 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001783 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001784 return RunGit(cmd) != ''
1785
1786
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001787def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001788 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001790 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 Updates changelog with metadata (e.g. pointer to review).
1792 Pushes/dcommits the code upstream.
1793 Updates review and closes.
1794 """
1795 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1796 help='bypass upload presubmit hook')
1797 parser.add_option('-m', dest='message',
1798 help="override review description")
1799 parser.add_option('-f', action='store_true', dest='force',
1800 help="force yes to questions (don't prompt)")
1801 parser.add_option('-c', dest='contributor',
1802 help="external contributor for patch (appended to " +
1803 "description and used as author for git). Should be " +
1804 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001805 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 (options, args) = parser.parse_args(args)
1807 cl = Changelist()
1808
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001809 current = cl.GetBranch()
1810 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1811 if not settings.GetIsGitSvn() and remote == '.':
1812 print
1813 print 'Attempting to push branch %r into another local branch!' % current
1814 print
1815 print 'Either reparent this branch on top of origin/master:'
1816 print ' git reparent-branch --root'
1817 print
1818 print 'OR run `git rebase-update` if you think the parent branch is already'
1819 print 'committed.'
1820 print
1821 print ' Current parent: %r' % upstream_branch
1822 return 1
1823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824 if not args or cmd == 'push':
1825 # Default to merging against our best guess of the upstream branch.
1826 args = [cl.GetUpstreamBranch()]
1827
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001828 if options.contributor:
1829 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1830 print "Please provide contibutor as 'First Last <email@example.com>'"
1831 return 1
1832
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001834 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835
ukai@chromium.org259e4682012-10-25 07:36:33 +00001836 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001837 return 1
1838
1839 # This rev-list syntax means "show all commits not in my branch that
1840 # are in base_branch".
1841 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1842 base_branch]).splitlines()
1843 if upstream_commits:
1844 print ('Base branch "%s" has %d commits '
1845 'not in this branch.' % (base_branch, len(upstream_commits)))
1846 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1847 return 1
1848
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001849 # This is the revision `svn dcommit` will commit on top of.
1850 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1851 '--pretty=format:%H'])
1852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001854 # If the base_head is a submodule merge commit, the first parent of the
1855 # base_head should be a git-svn commit, which is what we're interested in.
1856 base_svn_head = base_branch
1857 if base_has_submodules:
1858 base_svn_head += '^1'
1859
1860 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861 if extra_commits:
1862 print ('This branch has %d additional commits not upstreamed yet.'
1863 % len(extra_commits.splitlines()))
1864 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1865 'before attempting to %s.' % (base_branch, cmd))
1866 return 1
1867
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001868 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001869 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001870 author = None
1871 if options.contributor:
1872 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001873 hook_results = cl.RunHook(
1874 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001875 may_prompt=not options.force,
1876 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001877 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001878 if not hook_results.should_continue():
1879 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880
1881 if cmd == 'dcommit':
1882 # Check the tree status if the tree status URL is set.
1883 status = GetTreeStatus()
1884 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001885 print('The tree is closed. Please wait for it to reopen. Use '
1886 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887 return 1
1888 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001889 print('Unable to determine tree status. Please verify manually and '
1890 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001891 else:
1892 breakpad.SendStack(
1893 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001894 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1895 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001896 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001898 change_desc = ChangeDescription(options.message)
1899 if not change_desc.description and cl.GetIssue():
1900 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001902 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001903 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001904 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001905 else:
1906 print 'No description set.'
1907 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1908 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001910 # Keep a separate copy for the commit message, because the commit message
1911 # contains the link to the Rietveld issue, while the Rietveld message contains
1912 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001913 # Keep a separate copy for the commit message.
1914 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001915 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001916
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001917 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001918 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001919 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001921 commit_desc.append_footer('Patch from %s.' % options.contributor)
1922
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001923 print('Description:')
1924 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925
1926 branches = [base_branch, cl.GetBranchRef()]
1927 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001928 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001929 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001931 # We want to squash all this branch's commits into one commit with the proper
1932 # description. We do this by doing a "reset --soft" to the base branch (which
1933 # keeps the working copy the same), then dcommitting that. If origin/master
1934 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1935 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001937 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1938 # Delete the branches if they exist.
1939 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1940 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1941 result = RunGitWithCode(showref_cmd)
1942 if result[0] == 0:
1943 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001944
1945 # We might be in a directory that's present in this branch but not in the
1946 # trunk. Move up to the top of the tree so that git commands that expect a
1947 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001948 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949 if rel_base_path:
1950 os.chdir(rel_base_path)
1951
1952 # Stuff our change into the merge branch.
1953 # We wrap in a try...finally block so if anything goes wrong,
1954 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001955 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001956 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001957 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1958 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001960 RunGit(
1961 [
1962 'commit', '--author', options.contributor,
1963 '-m', commit_desc.description,
1964 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001966 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001967 if base_has_submodules:
1968 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1969 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1970 RunGit(['checkout', CHERRY_PICK_BRANCH])
1971 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001972 if cmd == 'push':
1973 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001974 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001975 retcode, output = RunGitWithCode(
1976 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1977 logging.debug(output)
1978 else:
1979 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001980 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001981 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001982 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983 finally:
1984 # And then swap back to the original branch and clean up.
1985 RunGit(['checkout', '-q', cl.GetBranch()])
1986 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001987 if base_has_submodules:
1988 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989
1990 if cl.GetIssue():
1991 if cmd == 'dcommit' and 'Committed r' in output:
1992 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1993 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001994 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1995 for l in output.splitlines(False))
1996 match = filter(None, match)
1997 if len(match) != 1:
1998 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1999 output)
2000 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001 else:
2002 return 1
2003 viewvc_url = settings.GetViewVCUrl()
2004 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002005 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00002006 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002007 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002008 print ('Closing issue '
2009 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002010 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002012 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002013 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00002014 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002015 if options.bypass_hooks:
2016 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2017 else:
2018 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002019 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002020 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002021
2022 if retcode == 0:
2023 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2024 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002025 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002026
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002027 return 0
2028
2029
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002030@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002031def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002032 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002034 message = """This doesn't appear to be an SVN repository.
2035If your project has a git mirror with an upstream SVN master, you probably need
2036to run 'git svn init', see your project's git mirror documentation.
2037If your project has a true writeable upstream repository, you probably want
2038to run 'git cl push' instead.
2039Choose wisely, if you get this wrong, your commit might appear to succeed but
2040will instead be silently ignored."""
2041 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002042 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043 return SendUpstream(parser, args, 'dcommit')
2044
2045
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002046@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002047def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002048 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049 if settings.GetIsGitSvn():
2050 print('This appears to be an SVN repository.')
2051 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002052 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053 return SendUpstream(parser, args, 'push')
2054
2055
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002056@subcommand.usage('[upstream branch to apply against]')
2057def CMDpush(parser, args):
2058 """Temporary alias for 'land'.
2059 """
2060 print(
2061 "\n=======\n"
2062 "'git cl push' has been renamed to 'git cl land'.\n"
2063 "Currently they are treated as synonyms, but 'git cl push' will stop\n"
2064 "working after 2014/07/01.\n"
2065 "=======\n")
2066 now = datetime.datetime.utcfromtimestamp(time.time())
2067 if now > datetime.datetime(2014, 7, 1):
2068 print('******\nReally, you should not use this command anymore... \n'
2069 'Pausing 10 sec to help you remember :-)')
2070 for n in xrange(10):
2071 time.sleep(1)
2072 print('%s seconds...' % (n+1))
2073 print('******')
2074 return CMDland(parser, args)
2075
2076
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002077@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002078def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002079 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002080 parser.add_option('-b', dest='newbranch',
2081 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002082 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002084 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2085 help='Change to the directory DIR immediately, '
2086 'before doing anything else.')
2087 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002088 help='failed patches spew .rej files rather than '
2089 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2091 help="don't commit after patch applies")
2092 (options, args) = parser.parse_args(args)
2093 if len(args) != 1:
2094 parser.print_help()
2095 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002096 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002097
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002098 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002099 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002100
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002101 if options.newbranch:
2102 if options.force:
2103 RunGit(['branch', '-D', options.newbranch],
2104 stderr=subprocess2.PIPE, error_ok=True)
2105 RunGit(['checkout', '-b', options.newbranch,
2106 Changelist().GetUpstreamBranch()])
2107
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002108 return PatchIssue(issue_arg, options.reject, options.nocommit,
2109 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002110
2111
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002112def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002113 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002114 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002115 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002116 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002117 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002118 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002120 # Assume it's a URL to the patch. Default to https.
2121 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002122 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002123 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124 DieWithError('Must pass an issue ID or full URL for '
2125 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002126 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002127 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002128 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130 # Switch up to the top-level directory, if necessary, in preparation for
2131 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002132 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133 if top:
2134 os.chdir(top)
2135
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 # Git patches have a/ at the beginning of source paths. We strip that out
2137 # with a sed script rather than the -p flag to patch so we can feed either
2138 # Git or svn-style patches into the same apply command.
2139 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002140 try:
2141 patch_data = subprocess2.check_output(
2142 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2143 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002144 DieWithError('Git patch mungling failed.')
2145 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002146
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147 # We use "git apply" to apply the patch instead of "patch" so that we can
2148 # pick up file adds.
2149 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002150 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002151 if directory:
2152 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002153 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002155 elif IsGitVersionAtLeast('1.7.12'):
2156 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002157 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002158 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002159 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002160 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002161 DieWithError('Failed to apply the patch')
2162
2163 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002164 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2166 cl = Changelist()
2167 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002168 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002169 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002170 else:
2171 print "Patch applied to index."
2172 return 0
2173
2174
2175def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002176 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002177 # Provide a wrapper for git svn rebase to help avoid accidental
2178 # git svn dcommit.
2179 # It's the only command that doesn't use parser at all since we just defer
2180 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002181
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002182 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183
2184
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002185def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002186 """Fetches the tree status and returns either 'open', 'closed',
2187 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002188 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002189 if url:
2190 status = urllib2.urlopen(url).read().lower()
2191 if status.find('closed') != -1 or status == '0':
2192 return 'closed'
2193 elif status.find('open') != -1 or status == '1':
2194 return 'open'
2195 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002196 return 'unset'
2197
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002198
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002199def GetTreeStatusReason():
2200 """Fetches the tree status from a json url and returns the message
2201 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002202 url = settings.GetTreeStatusUrl()
2203 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002204 connection = urllib2.urlopen(json_url)
2205 status = json.loads(connection.read())
2206 connection.close()
2207 return status['message']
2208
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002209
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002210def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002211 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002212 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002213 status = GetTreeStatus()
2214 if 'unset' == status:
2215 print 'You must configure your tree status URL by running "git cl config".'
2216 return 2
2217
2218 print "The tree is %s" % status
2219 print
2220 print GetTreeStatusReason()
2221 if status != 'open':
2222 return 1
2223 return 0
2224
2225
maruel@chromium.org15192402012-09-06 12:38:29 +00002226def CMDtry(parser, args):
2227 """Triggers a try job through Rietveld."""
2228 group = optparse.OptionGroup(parser, "Try job options")
2229 group.add_option(
2230 "-b", "--bot", action="append",
2231 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2232 "times to specify multiple builders. ex: "
2233 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2234 "the try server waterfall for the builders name and the tests "
2235 "available. Can also be used to specify gtest_filter, e.g. "
2236 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2237 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002238 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002239 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002240 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002241 "-r", "--revision",
2242 help="Revision to use for the try job; default: the "
2243 "revision will be determined by the try server; see "
2244 "its waterfall for more info")
2245 group.add_option(
2246 "-c", "--clobber", action="store_true", default=False,
2247 help="Force a clobber before building; e.g. don't do an "
2248 "incremental build")
2249 group.add_option(
2250 "--project",
2251 help="Override which project to use. Projects are defined "
2252 "server-side to define what default bot set to use")
2253 group.add_option(
2254 "-t", "--testfilter", action="append", default=[],
2255 help=("Apply a testfilter to all the selected builders. Unless the "
2256 "builders configurations are similar, use multiple "
2257 "--bot <builder>:<test> arguments."))
2258 group.add_option(
2259 "-n", "--name", help="Try job name; default to current branch name")
2260 parser.add_option_group(group)
2261 options, args = parser.parse_args(args)
2262
2263 if args:
2264 parser.error('Unknown arguments: %s' % args)
2265
2266 cl = Changelist()
2267 if not cl.GetIssue():
2268 parser.error('Need to upload first')
2269
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002270 props = cl.GetIssueProperties()
2271 if props.get('private'):
2272 parser.error('Cannot use trybots with private issue')
2273
maruel@chromium.org15192402012-09-06 12:38:29 +00002274 if not options.name:
2275 options.name = cl.GetBranch()
2276
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002277 if options.bot and not options.master:
2278 parser.error('For manually specified bots please also specify the '
2279 'tryserver master, e.g. "-m tryserver.chromium".')
2280
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002281 def GetMasterMap():
2282 # Process --bot and --testfilter.
2283 if not options.bot:
2284 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002285
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002286 # Get try masters from PRESUBMIT.py files.
2287 masters = presubmit_support.DoGetTryMasters(
2288 change,
2289 change.LocalPaths(),
2290 settings.GetRoot(),
2291 None,
2292 None,
2293 options.verbose,
2294 sys.stdout)
2295 if masters:
2296 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002297
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002298 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2299 options.bot = presubmit_support.DoGetTrySlaves(
2300 change,
2301 change.LocalPaths(),
2302 settings.GetRoot(),
2303 None,
2304 None,
2305 options.verbose,
2306 sys.stdout)
2307 if not options.bot:
2308 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002309
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002310 builders_and_tests = {}
2311 # TODO(machenbach): The old style command-line options don't support
2312 # multiple try masters yet.
2313 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2314 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2315
2316 for bot in old_style:
2317 if ':' in bot:
2318 builder, tests = bot.split(':', 1)
2319 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2320 elif ',' in bot:
2321 parser.error('Specify one bot per --bot flag')
2322 else:
2323 builders_and_tests.setdefault(bot, []).append('defaulttests')
2324
2325 for bot, tests in new_style:
2326 builders_and_tests.setdefault(bot, []).extend(tests)
2327
2328 # Return a master map with one master to be backwards compatible. The
2329 # master name defaults to an empty string, which will cause the master
2330 # not to be set on rietveld (deprecated).
2331 return {options.master: builders_and_tests}
2332
2333 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002334
maruel@chromium.org15192402012-09-06 12:38:29 +00002335 if options.testfilter:
2336 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002337 masters = dict((master, dict(
2338 (b, forced_tests) for b, t in slaves.iteritems()
2339 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002340
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002341 for builders in masters.itervalues():
2342 if any('triggered' in b for b in builders):
2343 print >> sys.stderr, (
2344 'ERROR You are trying to send a job to a triggered bot. This type of'
2345 ' bot requires an\ninitial job from a parent (usually a builder). '
2346 'Instead send your job to the parent.\n'
2347 'Bot list: %s' % builders)
2348 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002349
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002350 patchset = cl.GetMostRecentPatchset()
2351 if patchset and patchset != cl.GetPatchset():
2352 print(
2353 '\nWARNING Mismatch between local config and server. Did a previous '
2354 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2355 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002356 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002357 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002358 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002359 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002360 except urllib2.HTTPError, e:
2361 if e.code == 404:
2362 print('404 from rietveld; '
2363 'did you mean to use "git try" instead of "git cl try"?')
2364 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002365 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002366
2367 for (master, builders) in masters.iteritems():
2368 if master:
2369 print 'Master: %s' % master
2370 length = max(len(builder) for builder in builders)
2371 for builder in sorted(builders):
2372 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002373 return 0
2374
2375
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002376@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002377def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002378 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002379 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002380 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002381 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002382
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002383 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002384 if args:
2385 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002386 branch = cl.GetBranch()
2387 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002388 cl = Changelist()
2389 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002390
2391 # Clear configured merge-base, if there is one.
2392 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002393 else:
2394 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002395 return 0
2396
2397
thestig@chromium.org00858c82013-12-02 23:08:03 +00002398def CMDweb(parser, args):
2399 """Opens the current CL in the web browser."""
2400 _, args = parser.parse_args(args)
2401 if args:
2402 parser.error('Unrecognized args: %s' % ' '.join(args))
2403
2404 issue_url = Changelist().GetIssueURL()
2405 if not issue_url:
2406 print >> sys.stderr, 'ERROR No issue to open'
2407 return 1
2408
2409 webbrowser.open(issue_url)
2410 return 0
2411
2412
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002413def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002414 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002415 _, args = parser.parse_args(args)
2416 if args:
2417 parser.error('Unrecognized args: %s' % ' '.join(args))
2418 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002419 props = cl.GetIssueProperties()
2420 if props.get('private'):
2421 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002422 cl.SetFlag('commit', '1')
2423 return 0
2424
2425
groby@chromium.org411034a2013-02-26 15:12:01 +00002426def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002427 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002428 _, args = parser.parse_args(args)
2429 if args:
2430 parser.error('Unrecognized args: %s' % ' '.join(args))
2431 cl = Changelist()
2432 # Ensure there actually is an issue to close.
2433 cl.GetDescription()
2434 cl.CloseIssue()
2435 return 0
2436
2437
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002438def CMDdiff(parser, args):
2439 """shows differences between local tree and last upload."""
2440 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002441 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002442 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002443 if not issue:
2444 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002445 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002446 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002447
2448 # Create a new branch based on the merge-base
2449 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2450 try:
2451 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002452 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002453 if rtn != 0:
2454 return rtn
2455
2456 # Switch back to starting brand and diff against the temporary
2457 # branch containing the latest rietveld patch.
2458 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2459 finally:
2460 RunGit(['checkout', '-q', branch])
2461 RunGit(['branch', '-D', TMP_BRANCH])
2462
2463 return 0
2464
2465
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002466def CMDowners(parser, args):
2467 """interactively find the owners for reviewing"""
2468 parser.add_option(
2469 '--no-color',
2470 action='store_true',
2471 help='Use this option to disable color output')
2472 options, args = parser.parse_args(args)
2473
2474 author = RunGit(['config', 'user.email']).strip() or None
2475
2476 cl = Changelist()
2477
2478 if args:
2479 if len(args) > 1:
2480 parser.error('Unknown args')
2481 base_branch = args[0]
2482 else:
2483 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002484 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002485
2486 change = cl.GetChange(base_branch, None)
2487 return owners_finder.OwnersFinder(
2488 [f.LocalPath() for f in
2489 cl.GetChange(base_branch, None).AffectedFiles()],
2490 change.RepositoryRoot(), author,
2491 fopen=file, os_path=os.path, glob=glob.glob,
2492 disable_color=options.no_color).run()
2493
2494
enne@chromium.org555cfe42014-01-29 18:21:39 +00002495@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002496def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002497 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002498 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002499 parser.add_option('--full', action='store_true',
2500 help='Reformat the full content of all touched files')
2501 parser.add_option('--dry-run', action='store_true',
2502 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002503 parser.add_option('--diff', action='store_true',
2504 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002505 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002506
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002507 # git diff generates paths against the root of the repository. Change
2508 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002509 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002510 if rel_base_path:
2511 os.chdir(rel_base_path)
2512
digit@chromium.org29e47272013-05-17 17:01:46 +00002513 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002514 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002515 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002516 # Only list the names of modified files.
2517 diff_cmd.append('--name-only')
2518 else:
2519 # Only generate context-less patches.
2520 diff_cmd.append('-U0')
2521
2522 # Grab the merge-base commit, i.e. the upstream commit of the current
2523 # branch when it was created or the last time it was rebased. This is
2524 # to cover the case where the user may have called "git fetch origin",
2525 # moving the origin branch to a newer commit, but hasn't rebased yet.
2526 upstream_commit = None
2527 cl = Changelist()
2528 upstream_branch = cl.GetUpstreamBranch()
2529 if upstream_branch:
2530 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2531 upstream_commit = upstream_commit.strip()
2532
2533 if not upstream_commit:
2534 DieWithError('Could not find base commit for this branch. '
2535 'Are you in detached state?')
2536
2537 diff_cmd.append(upstream_commit)
2538
2539 # Handle source file filtering.
2540 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002541 if args:
2542 for arg in args:
2543 if os.path.isdir(arg):
2544 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2545 elif os.path.isfile(arg):
2546 diff_cmd.append(arg)
2547 else:
2548 DieWithError('Argument "%s" is not a file or a directory' % arg)
2549 else:
2550 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002551 diff_output = RunGit(diff_cmd)
2552
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002553 top_dir = os.path.normpath(
2554 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2555
2556 # Locate the clang-format binary in the checkout
2557 try:
2558 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2559 except clang_format.NotFoundError, e:
2560 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002561
digit@chromium.org29e47272013-05-17 17:01:46 +00002562 if opts.full:
2563 # diff_output is a list of files to send to clang-format.
2564 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002565 if not files:
2566 print "Nothing to format."
2567 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002568 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002569 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002570 cmd.append('-i')
2571 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002572 if opts.diff:
2573 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002574 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002575 env = os.environ.copy()
2576 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002577 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002578 try:
2579 script = clang_format.FindClangFormatScriptInChromiumTree(
2580 'clang-format-diff.py')
2581 except clang_format.NotFoundError, e:
2582 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002583
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002584 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002585 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002586 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002587
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002588 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002589 if opts.diff:
2590 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002591 if opts.dry_run and len(stdout) > 0:
2592 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002593
2594 return 0
2595
2596
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002597class OptionParser(optparse.OptionParser):
2598 """Creates the option parse and add --verbose support."""
2599 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002600 optparse.OptionParser.__init__(
2601 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002602 self.add_option(
2603 '-v', '--verbose', action='count', default=0,
2604 help='Use 2 times for more debugging info')
2605
2606 def parse_args(self, args=None, values=None):
2607 options, args = optparse.OptionParser.parse_args(self, args, values)
2608 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2609 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2610 return options, args
2611
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002612
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002613def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002614 if sys.hexversion < 0x02060000:
2615 print >> sys.stderr, (
2616 '\nYour python version %s is unsupported, please upgrade.\n' %
2617 sys.version.split(' ', 1)[0])
2618 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002619
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002620 # Reload settings.
2621 global settings
2622 settings = Settings()
2623
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002624 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002625 dispatcher = subcommand.CommandDispatcher(__name__)
2626 try:
2627 return dispatcher.execute(OptionParser(), argv)
2628 except urllib2.HTTPError, e:
2629 if e.code != 500:
2630 raise
2631 DieWithError(
2632 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2633 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002634
2635
2636if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002637 # These affect sys.stdout so do it outside of main() to simplify mocks in
2638 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002639 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002640 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002641 sys.exit(main(sys.argv[1:]))