blob: fe9fec76035fc1385de200928a440f125e9f074c [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000284
285 def LazyUpdateIfNeeded(self):
286 """Updates the settings from a codereview.settings file, if available."""
287 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000288 # The only value that actually changes the behavior is
289 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000290 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000291 error_ok=True
292 ).strip().lower()
293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000294 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000295 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000297 # set updated to True to avoid infinite calling loop
298 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000299 self.updated = True
300 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000301 self.updated = True
302
303 def GetDefaultServerUrl(self, error_ok=False):
304 if not self.default_server:
305 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000306 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000307 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000308 if error_ok:
309 return self.default_server
310 if not self.default_server:
311 error_message = ('Could not find settings file. You must configure '
312 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000313 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000314 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000315 return self.default_server
316
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000317 @staticmethod
318 def GetRelativeRoot():
319 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000320
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000322 if self.root is None:
323 self.root = os.path.abspath(self.GetRelativeRoot())
324 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000325
326 def GetIsGitSvn(self):
327 """Return true if this repo looks like it's using git-svn."""
328 if self.is_git_svn is None:
329 # If you have any "svn-remote.*" config keys, we think you're using svn.
330 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000331 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000332 return self.is_git_svn
333
334 def GetSVNBranch(self):
335 if self.svn_branch is None:
336 if not self.GetIsGitSvn():
337 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
338
339 # Try to figure out which remote branch we're based on.
340 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000341 # 1) iterate through our branch history and find the svn URL.
342 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000343
344 # regexp matching the git-svn line that contains the URL.
345 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
346
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000347 # We don't want to go through all of history, so read a line from the
348 # pipe at a time.
349 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000350 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000351 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
352 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000353 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000354 for line in proc.stdout:
355 match = git_svn_re.match(line)
356 if match:
357 url = match.group(1)
358 proc.stdout.close() # Cut pipe.
359 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000360
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000361 if url:
362 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
363 remotes = RunGit(['config', '--get-regexp',
364 r'^svn-remote\..*\.url']).splitlines()
365 for remote in remotes:
366 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000367 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000368 remote = match.group(1)
369 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000370 rewrite_root = RunGit(
371 ['config', 'svn-remote.%s.rewriteRoot' % remote],
372 error_ok=True).strip()
373 if rewrite_root:
374 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000375 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000376 ['config', 'svn-remote.%s.fetch' % remote],
377 error_ok=True).strip()
378 if fetch_spec:
379 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
380 if self.svn_branch:
381 break
382 branch_spec = RunGit(
383 ['config', 'svn-remote.%s.branches' % remote],
384 error_ok=True).strip()
385 if branch_spec:
386 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
387 if self.svn_branch:
388 break
389 tag_spec = RunGit(
390 ['config', 'svn-remote.%s.tags' % remote],
391 error_ok=True).strip()
392 if tag_spec:
393 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
394 if self.svn_branch:
395 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000396
397 if not self.svn_branch:
398 DieWithError('Can\'t guess svn branch -- try specifying it on the '
399 'command line')
400
401 return self.svn_branch
402
403 def GetTreeStatusUrl(self, error_ok=False):
404 if not self.tree_status_url:
405 error_message = ('You must configure your tree status URL by running '
406 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000407 self.tree_status_url = self._GetRietveldConfig(
408 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000409 return self.tree_status_url
410
411 def GetViewVCUrl(self):
412 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000413 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 return self.viewvc_url
415
rmistry@google.com90752582014-01-14 21:04:50 +0000416 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000417 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000418
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000419 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000420 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000421
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000422 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000423 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000424
ukai@chromium.orge8077812012-02-03 03:41:46 +0000425 def GetIsGerrit(self):
426 """Return true if this repo is assosiated with gerrit code review system."""
427 if self.is_gerrit is None:
428 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
429 return self.is_gerrit
430
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000431 def GetGitEditor(self):
432 """Return the editor specified in the git config, or None if none is."""
433 if self.git_editor is None:
434 self.git_editor = self._GetConfig('core.editor', error_ok=True)
435 return self.git_editor or None
436
thestig@chromium.org44202a22014-03-11 19:22:18 +0000437 def GetLintRegex(self):
438 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
439 DEFAULT_LINT_REGEX)
440
441 def GetLintIgnoreRegex(self):
442 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
443 DEFAULT_LINT_IGNORE_REGEX)
444
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000445 def _GetRietveldConfig(self, param, **kwargs):
446 return self._GetConfig('rietveld.' + param, **kwargs)
447
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000448 def _GetConfig(self, param, **kwargs):
449 self.LazyUpdateIfNeeded()
450 return RunGit(['config', param], **kwargs).strip()
451
452
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453def ShortBranchName(branch):
454 """Convert a name like 'refs/heads/foo' to just 'foo'."""
455 return branch.replace('refs/heads/', '')
456
457
458class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000459 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000460 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000461 global settings
462 if not settings:
463 # Happens when git_cl.py is used as a utility library.
464 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 settings.GetDefaultServerUrl()
466 self.branchref = branchref
467 if self.branchref:
468 self.branch = ShortBranchName(self.branchref)
469 else:
470 self.branch = None
471 self.rietveld_server = None
472 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000473 self.lookedup_issue = False
474 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000475 self.has_description = False
476 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000477 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000479 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000480 self.cc = None
481 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000482 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000483 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000484
485 def GetCCList(self):
486 """Return the users cc'd on this CL.
487
488 Return is a string suitable for passing to gcl with the --cc flag.
489 """
490 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000491 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000492 more_cc = ','.join(self.watchers)
493 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
494 return self.cc
495
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000496 def GetCCListWithoutDefault(self):
497 """Return the users cc'd on this CL excluding default ones."""
498 if self.cc is None:
499 self.cc = ','.join(self.watchers)
500 return self.cc
501
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000502 def SetWatchers(self, watchers):
503 """Set the list of email addresses that should be cc'd based on the changed
504 files in this CL.
505 """
506 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507
508 def GetBranch(self):
509 """Returns the short branch name, e.g. 'master'."""
510 if not self.branch:
511 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
512 self.branch = ShortBranchName(self.branchref)
513 return self.branch
514
515 def GetBranchRef(self):
516 """Returns the full branch name, e.g. 'refs/heads/master'."""
517 self.GetBranch() # Poke the lazy loader.
518 return self.branchref
519
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000520 @staticmethod
521 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000522 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000523 e.g. 'origin', 'refs/heads/master'
524 """
525 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000526 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
527 error_ok=True).strip()
528 if upstream_branch:
529 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
530 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000531 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
532 error_ok=True).strip()
533 if upstream_branch:
534 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000535 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000536 # Fall back on trying a git-svn upstream branch.
537 if settings.GetIsGitSvn():
538 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000540 # Else, try to guess the origin remote.
541 remote_branches = RunGit(['branch', '-r']).split()
542 if 'origin/master' in remote_branches:
543 # Fall back on origin/master if it exits.
544 remote = 'origin'
545 upstream_branch = 'refs/heads/master'
546 elif 'origin/trunk' in remote_branches:
547 # Fall back on origin/trunk if it exists. Generally a shared
548 # git-svn clone
549 remote = 'origin'
550 upstream_branch = 'refs/heads/trunk'
551 else:
552 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000553Either pass complete "git diff"-style arguments, like
554 git cl upload origin/master
555or verify this branch is set up to track another (via the --track argument to
556"git checkout -b ...").""")
557
558 return remote, upstream_branch
559
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000560 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000561 return git_common.get_or_create_merge_base(self.GetBranch(),
562 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000563
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000564 def GetUpstreamBranch(self):
565 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000566 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000567 if remote is not '.':
568 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
569 self.upstream_branch = upstream_branch
570 return self.upstream_branch
571
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000572 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000573 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000574 remote, branch = None, self.GetBranch()
575 seen_branches = set()
576 while branch not in seen_branches:
577 seen_branches.add(branch)
578 remote, branch = self.FetchUpstreamTuple(branch)
579 branch = ShortBranchName(branch)
580 if remote != '.' or branch.startswith('refs/remotes'):
581 break
582 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000583 remotes = RunGit(['remote'], error_ok=True).split()
584 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000585 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000586 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000587 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000588 logging.warning('Could not determine which remote this change is '
589 'associated with, so defaulting to "%s". This may '
590 'not be what you want. You may prevent this message '
591 'by running "git svn info" as documented here: %s',
592 self._remote,
593 GIT_INSTRUCTIONS_URL)
594 else:
595 logging.warn('Could not determine which remote this change is '
596 'associated with. You may prevent this message by '
597 'running "git svn info" as documented here: %s',
598 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000599 branch = 'HEAD'
600 if branch.startswith('refs/remotes'):
601 self._remote = (remote, branch)
602 else:
603 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000604 return self._remote
605
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000606 def GitSanityChecks(self, upstream_git_obj):
607 """Checks git repo status and ensures diff is from local commits."""
608
609 # Verify the commit we're diffing against is in our current branch.
610 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
611 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
612 if upstream_sha != common_ancestor:
613 print >> sys.stderr, (
614 'ERROR: %s is not in the current branch. You may need to rebase '
615 'your tracking branch' % upstream_sha)
616 return False
617
618 # List the commits inside the diff, and verify they are all local.
619 commits_in_diff = RunGit(
620 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
621 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
622 remote_branch = remote_branch.strip()
623 if code != 0:
624 _, remote_branch = self.GetRemoteBranch()
625
626 commits_in_remote = RunGit(
627 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
628
629 common_commits = set(commits_in_diff) & set(commits_in_remote)
630 if common_commits:
631 print >> sys.stderr, (
632 'ERROR: Your diff contains %d commits already in %s.\n'
633 'Run "git log --oneline %s..HEAD" to get a list of commits in '
634 'the diff. If you are using a custom git flow, you can override'
635 ' the reference used for this check with "git config '
636 'gitcl.remotebranch <git-ref>".' % (
637 len(common_commits), remote_branch, upstream_git_obj))
638 return False
639 return True
640
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000641 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000642 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000643
644 Returns None if it is not set.
645 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000646 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
647 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 def GetRemoteUrl(self):
650 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
651
652 Returns None if there is no remote.
653 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000654 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
656
657 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000658 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000659 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000661 self.issue = int(issue) or None if issue else None
662 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000663 return self.issue
664
665 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000666 if not self.rietveld_server:
667 # If we're on a branch then get the server potentially associated
668 # with that branch.
669 if self.GetIssue():
670 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
671 ['config', self._RietveldServer()], error_ok=True).strip())
672 if not self.rietveld_server:
673 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674 return self.rietveld_server
675
676 def GetIssueURL(self):
677 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000678 if not self.GetIssue():
679 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
681
682 def GetDescription(self, pretty=False):
683 if not self.has_description:
684 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000685 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000686 try:
687 self.description = self.RpcServer().get_description(issue).strip()
688 except urllib2.HTTPError, e:
689 if e.code == 404:
690 DieWithError(
691 ('\nWhile fetching the description for issue %d, received a '
692 '404 (not found)\n'
693 'error. It is likely that you deleted this '
694 'issue on the server. If this is the\n'
695 'case, please run\n\n'
696 ' git cl issue 0\n\n'
697 'to clear the association with the deleted issue. Then run '
698 'this command again.') % issue)
699 else:
700 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000701 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702 self.has_description = True
703 if pretty:
704 wrapper = textwrap.TextWrapper()
705 wrapper.initial_indent = wrapper.subsequent_indent = ' '
706 return wrapper.fill(self.description)
707 return self.description
708
709 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000710 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000711 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712 patchset = RunGit(['config', self._PatchsetSetting()],
713 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000714 self.patchset = int(patchset) or None if patchset else None
715 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716 return self.patchset
717
718 def SetPatchset(self, patchset):
719 """Set this branch's patchset. If patchset=0, clears the patchset."""
720 if patchset:
721 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000722 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723 else:
724 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000725 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000726 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000728 def GetMostRecentPatchset(self):
729 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000730
731 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000732 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000733 '/download/issue%s_%s.diff' % (issue, patchset))
734
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000735 def GetIssueProperties(self):
736 if self._props is None:
737 issue = self.GetIssue()
738 if not issue:
739 self._props = {}
740 else:
741 self._props = self.RpcServer().get_issue_properties(issue, True)
742 return self._props
743
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000744 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000745 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 def SetIssue(self, issue):
748 """Set this branch's issue. If issue=0, clears the issue."""
749 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000750 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 RunGit(['config', self._IssueSetting(), str(issue)])
752 if self.rietveld_server:
753 RunGit(['config', self._RietveldServer(), self.rietveld_server])
754 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000755 current_issue = self.GetIssue()
756 if current_issue:
757 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000758 self.issue = None
759 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000761 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000762 if not self.GitSanityChecks(upstream_branch):
763 DieWithError('\nGit sanity check failure')
764
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000766 if not root:
767 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000768 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000769
770 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000771 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000772 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000773 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000774 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000775 except subprocess2.CalledProcessError:
776 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000777 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000778 'This branch probably doesn\'t exist anymore. To reset the\n'
779 'tracking branch, please run\n'
780 ' git branch --set-upstream %s trunk\n'
781 'replacing trunk with origin/master or the relevant branch') %
782 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000783
maruel@chromium.org52424302012-08-29 15:14:30 +0000784 issue = self.GetIssue()
785 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000786 if issue:
787 description = self.GetDescription()
788 else:
789 # If the change was never uploaded, use the log messages of all commits
790 # up to the branch point, as git cl upload will prefill the description
791 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000792 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
793 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000794
795 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000796 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000797 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000798 name,
799 description,
800 absroot,
801 files,
802 issue,
803 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000804 author,
805 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000806
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000807 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000808 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000809
810 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000811 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000812 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000813 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000814 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000815 except presubmit_support.PresubmitFailure, e:
816 DieWithError(
817 ('%s\nMaybe your depot_tools is out of date?\n'
818 'If all fails, contact maruel@') % e)
819
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000820 def UpdateDescription(self, description):
821 self.description = description
822 return self.RpcServer().update_description(
823 self.GetIssue(), self.description)
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000826 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000827 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000829 def SetFlag(self, flag, value):
830 """Patchset must match."""
831 if not self.GetPatchset():
832 DieWithError('The patchset needs to match. Send another patchset.')
833 try:
834 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000835 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000836 except urllib2.HTTPError, e:
837 if e.code == 404:
838 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
839 if e.code == 403:
840 DieWithError(
841 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
842 'match?') % (self.GetIssue(), self.GetPatchset()))
843 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000845 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 """Returns an upload.RpcServer() to access this review's rietveld instance.
847 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000848 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000849 self._rpc_server = rietveld.CachingRietveld(
850 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000851 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
853 def _IssueSetting(self):
854 """Return the git setting that stores this change's issue."""
855 return 'branch.%s.rietveldissue' % self.GetBranch()
856
857 def _PatchsetSetting(self):
858 """Return the git setting that stores this change's most recent patchset."""
859 return 'branch.%s.rietveldpatchset' % self.GetBranch()
860
861 def _RietveldServer(self):
862 """Returns the git setting that stores this change's rietveld server."""
863 return 'branch.%s.rietveldserver' % self.GetBranch()
864
865
866def GetCodereviewSettingsInteractively():
867 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000868 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 server = settings.GetDefaultServerUrl(error_ok=True)
870 prompt = 'Rietveld server (host[:port])'
871 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000872 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 if not server and not newserver:
874 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000875 if newserver:
876 newserver = gclient_utils.UpgradeToHttps(newserver)
877 if newserver != server:
878 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000880 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 prompt = caption
882 if initial:
883 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000884 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885 if new_val == 'x':
886 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000887 elif new_val:
888 if is_url:
889 new_val = gclient_utils.UpgradeToHttps(new_val)
890 if new_val != initial:
891 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000893 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000894 SetProperty(settings.GetDefaultPrivateFlag(),
895 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000897 'tree-status-url', False)
898 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000899 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900
901 # TODO: configure a default branch to diff against, rather than this
902 # svn-based hackery.
903
904
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000905class ChangeDescription(object):
906 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000907 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000908 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000909
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000910 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000911 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000912
agable@chromium.org42c20792013-09-12 17:34:49 +0000913 @property # www.logilab.org/ticket/89786
914 def description(self): # pylint: disable=E0202
915 return '\n'.join(self._description_lines)
916
917 def set_description(self, desc):
918 if isinstance(desc, basestring):
919 lines = desc.splitlines()
920 else:
921 lines = [line.rstrip() for line in desc]
922 while lines and not lines[0]:
923 lines.pop(0)
924 while lines and not lines[-1]:
925 lines.pop(-1)
926 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000927
928 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000929 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000930 assert isinstance(reviewers, list), reviewers
931 if not reviewers:
932 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000933 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000934
agable@chromium.org42c20792013-09-12 17:34:49 +0000935 # Get the set of R= and TBR= lines and remove them from the desciption.
936 regexp = re.compile(self.R_LINE)
937 matches = [regexp.match(line) for line in self._description_lines]
938 new_desc = [l for i, l in enumerate(self._description_lines)
939 if not matches[i]]
940 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000941
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 # Construct new unified R= and TBR= lines.
943 r_names = []
944 tbr_names = []
945 for match in matches:
946 if not match:
947 continue
948 people = cleanup_list([match.group(2).strip()])
949 if match.group(1) == 'TBR':
950 tbr_names.extend(people)
951 else:
952 r_names.extend(people)
953 for name in r_names:
954 if name not in reviewers:
955 reviewers.append(name)
956 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
957 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
958
959 # Put the new lines in the description where the old first R= line was.
960 line_loc = next((i for i, match in enumerate(matches) if match), -1)
961 if 0 <= line_loc < len(self._description_lines):
962 if new_tbr_line:
963 self._description_lines.insert(line_loc, new_tbr_line)
964 if new_r_line:
965 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000966 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000967 if new_r_line:
968 self.append_footer(new_r_line)
969 if new_tbr_line:
970 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000971
972 def prompt(self):
973 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000974 self.set_description([
975 '# Enter a description of the change.',
976 '# This will be displayed on the codereview site.',
977 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000978 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000979 '--------------------',
980 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000981
agable@chromium.org42c20792013-09-12 17:34:49 +0000982 regexp = re.compile(self.BUG_LINE)
983 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000984 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000985 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000986 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000987 if not content:
988 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000989 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000990
991 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000992 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
993 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000994 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000995 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000996
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000997 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000998 if self._description_lines:
999 # Add an empty line if either the last line or the new line isn't a tag.
1000 last_line = self._description_lines[-1]
1001 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1002 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1003 self._description_lines.append('')
1004 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001005
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001006 def get_reviewers(self):
1007 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001008 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1009 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001010 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001011
1012
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001013def get_approving_reviewers(props):
1014 """Retrieves the reviewers that approved a CL from the issue properties with
1015 messages.
1016
1017 Note that the list may contain reviewers that are not committer, thus are not
1018 considered by the CQ.
1019 """
1020 return sorted(
1021 set(
1022 message['sender']
1023 for message in props['messages']
1024 if message['approval'] and message['sender'] in props['reviewers']
1025 )
1026 )
1027
1028
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029def FindCodereviewSettingsFile(filename='codereview.settings'):
1030 """Finds the given file starting in the cwd and going up.
1031
1032 Only looks up to the top of the repository unless an
1033 'inherit-review-settings-ok' file exists in the root of the repository.
1034 """
1035 inherit_ok_file = 'inherit-review-settings-ok'
1036 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001037 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1039 root = '/'
1040 while True:
1041 if filename in os.listdir(cwd):
1042 if os.path.isfile(os.path.join(cwd, filename)):
1043 return open(os.path.join(cwd, filename))
1044 if cwd == root:
1045 break
1046 cwd = os.path.dirname(cwd)
1047
1048
1049def LoadCodereviewSettingsFromFile(fileobj):
1050 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001051 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 def SetProperty(name, setting, unset_error_ok=False):
1054 fullname = 'rietveld.' + name
1055 if setting in keyvals:
1056 RunGit(['config', fullname, keyvals[setting]])
1057 else:
1058 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1059
1060 SetProperty('server', 'CODE_REVIEW_SERVER')
1061 # Only server setting is required. Other settings can be absent.
1062 # In that case, we ignore errors raised during option deletion attempt.
1063 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001064 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1066 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001067 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001068 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1069 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001071 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001072 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1075 #should be of the form
1076 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1077 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1078 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1079 keyvals['ORIGIN_URL_CONFIG']])
1080
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001082def urlretrieve(source, destination):
1083 """urllib is broken for SSL connections via a proxy therefore we
1084 can't use urllib.urlretrieve()."""
1085 with open(destination, 'w') as f:
1086 f.write(urllib2.urlopen(source).read())
1087
1088
ukai@chromium.org712d6102013-11-27 00:52:58 +00001089def hasSheBang(fname):
1090 """Checks fname is a #! script."""
1091 with open(fname) as f:
1092 return f.read(2).startswith('#!')
1093
1094
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001095def DownloadHooks(force):
1096 """downloads hooks
1097
1098 Args:
1099 force: True to update hooks. False to install hooks if not present.
1100 """
1101 if not settings.GetIsGerrit():
1102 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001103 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001104 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1105 if not os.access(dst, os.X_OK):
1106 if os.path.exists(dst):
1107 if not force:
1108 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001109 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001110 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001111 if not hasSheBang(dst):
1112 DieWithError('Not a script: %s\n'
1113 'You need to download from\n%s\n'
1114 'into .git/hooks/commit-msg and '
1115 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001116 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1117 except Exception:
1118 if os.path.exists(dst):
1119 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001120 DieWithError('\nFailed to download hooks.\n'
1121 'You need to download from\n%s\n'
1122 'into .git/hooks/commit-msg and '
1123 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001124
1125
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001126@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001128 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001130 parser.add_option('--activate-update', action='store_true',
1131 help='activate auto-updating [rietveld] section in '
1132 '.git/config')
1133 parser.add_option('--deactivate-update', action='store_true',
1134 help='deactivate auto-updating [rietveld] section in '
1135 '.git/config')
1136 options, args = parser.parse_args(args)
1137
1138 if options.deactivate_update:
1139 RunGit(['config', 'rietveld.autoupdate', 'false'])
1140 return
1141
1142 if options.activate_update:
1143 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1144 return
1145
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 if len(args) == 0:
1147 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001148 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 return 0
1150
1151 url = args[0]
1152 if not url.endswith('codereview.settings'):
1153 url = os.path.join(url, 'codereview.settings')
1154
1155 # Load code review settings and download hooks (if available).
1156 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001157 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 return 0
1159
1160
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001161def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001162 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001163 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1164 branch = ShortBranchName(branchref)
1165 _, args = parser.parse_args(args)
1166 if not args:
1167 print("Current base-url:")
1168 return RunGit(['config', 'branch.%s.base-url' % branch],
1169 error_ok=False).strip()
1170 else:
1171 print("Setting base-url to %s" % args[0])
1172 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1173 error_ok=False).strip()
1174
1175
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001177 """Show status of changelists.
1178
1179 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001180 - Red not sent for review or broken
1181 - Blue waiting for review
1182 - Yellow waiting for you to reply to review
1183 - Green LGTM'ed
1184 - Magenta in the commit queue
1185 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001186
1187 Also see 'git cl comments'.
1188 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 parser.add_option('--field',
1190 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001191 parser.add_option('-f', '--fast', action='store_true',
1192 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001194 if args:
1195 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001198 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199 if options.field.startswith('desc'):
1200 print cl.GetDescription()
1201 elif options.field == 'id':
1202 issueid = cl.GetIssue()
1203 if issueid:
1204 print issueid
1205 elif options.field == 'patch':
1206 patchset = cl.GetPatchset()
1207 if patchset:
1208 print patchset
1209 elif options.field == 'url':
1210 url = cl.GetIssueURL()
1211 if url:
1212 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001213 return 0
1214
1215 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1216 if not branches:
1217 print('No local branch found.')
1218 return 0
1219
1220 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001221 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001222 alignment = max(5, max(len(b) for b in branches))
1223 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224 # Adhoc thread pool to request data concurrently.
1225 output = Queue.Queue()
1226
1227 # Silence upload.py otherwise it becomes unweldly.
1228 upload.verbosity = 0
1229
1230 if not options.fast:
1231 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001232 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233 c = Changelist(branchref=b)
1234 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001235 props = {}
1236 r = None
1237 if i:
1238 try:
1239 props = c.GetIssueProperties()
1240 r = c.GetApprovingReviewers() if i else None
1241 except urllib2.HTTPError:
1242 # The issue probably doesn't exist anymore.
1243 i += ' (broken)'
1244
1245 msgs = props.get('messages') or []
1246
1247 if not i:
1248 color = Fore.WHITE
1249 elif props.get('closed'):
1250 # Issue is closed.
1251 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001252 elif props.get('commit'):
1253 # Issue is in the commit queue.
1254 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001255 elif r:
1256 # Was LGTM'ed.
1257 color = Fore.GREEN
1258 elif not msgs:
1259 # No message was sent.
1260 color = Fore.RED
1261 elif msgs[-1]['sender'] != props.get('owner_email'):
1262 color = Fore.YELLOW
1263 else:
1264 color = Fore.BLUE
1265 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001266
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001267 # Process one branch synchronously to work through authentication, then
1268 # spawn threads to process all the other branches in parallel.
1269 if branches:
1270 fetch(branches[0])
1271 threads = [
1272 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001273 for t in threads:
1274 t.daemon = True
1275 t.start()
1276 else:
1277 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1278 for b in branches:
1279 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001280 url = c.GetIssueURL()
1281 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282
1283 tmp = {}
1284 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001285 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001286 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001287 b, i, color = output.get()
1288 tmp[b] = (i, color)
1289 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001290 reset = Fore.RESET
1291 if not sys.stdout.isatty():
1292 color = ''
1293 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001294 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001295 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001296
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001297 cl = Changelist()
1298 print
1299 print 'Current branch:',
1300 if not cl.GetIssue():
1301 print 'no issue assigned.'
1302 return 0
1303 print cl.GetBranch()
1304 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1305 print 'Issue description:'
1306 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307 return 0
1308
1309
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001310def colorize_CMDstatus_doc():
1311 """To be called once in main() to add colors to git cl status help."""
1312 colors = [i for i in dir(Fore) if i[0].isupper()]
1313
1314 def colorize_line(line):
1315 for color in colors:
1316 if color in line.upper():
1317 # Extract whitespaces first and the leading '-'.
1318 indent = len(line) - len(line.lstrip(' ')) + 1
1319 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1320 return line
1321
1322 lines = CMDstatus.__doc__.splitlines()
1323 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1324
1325
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001326@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001328 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329
1330 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001331 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001332 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333
1334 cl = Changelist()
1335 if len(args) > 0:
1336 try:
1337 issue = int(args[0])
1338 except ValueError:
1339 DieWithError('Pass a number to set the issue or none to list it.\n'
1340 'Maybe you want to run git cl status?')
1341 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001342 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 return 0
1344
1345
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001346def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001347 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001348 (_, args) = parser.parse_args(args)
1349 if args:
1350 parser.error('Unsupported argument: %s' % args)
1351
1352 cl = Changelist()
1353 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001354 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001355 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001356 if message['disapproval']:
1357 color = Fore.RED
1358 elif message['approval']:
1359 color = Fore.GREEN
1360 elif message['sender'] == data['owner_email']:
1361 color = Fore.MAGENTA
1362 else:
1363 color = Fore.BLUE
1364 print '\n%s%s %s%s' % (
1365 color, message['date'].split('.', 1)[0], message['sender'],
1366 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001367 if message['text'].strip():
1368 print '\n'.join(' ' + l for l in message['text'].splitlines())
1369 return 0
1370
1371
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001372def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001373 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001374 cl = Changelist()
1375 if not cl.GetIssue():
1376 DieWithError('This branch has no associated changelist.')
1377 description = ChangeDescription(cl.GetDescription())
1378 description.prompt()
1379 cl.UpdateDescription(description.description)
1380 return 0
1381
1382
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383def CreateDescriptionFromLog(args):
1384 """Pulls out the commit log to use as a base for the CL description."""
1385 log_args = []
1386 if len(args) == 1 and not args[0].endswith('.'):
1387 log_args = [args[0] + '..']
1388 elif len(args) == 1 and args[0].endswith('...'):
1389 log_args = [args[0][:-1]]
1390 elif len(args) == 2:
1391 log_args = [args[0] + '..' + args[1]]
1392 else:
1393 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001394 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
1396
thestig@chromium.org44202a22014-03-11 19:22:18 +00001397def CMDlint(parser, args):
1398 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001399 parser.add_option('--filter', action='append', metavar='-x,+y',
1400 help='Comma-separated list of cpplint\'s category-filters')
1401 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001402
1403 # Access to a protected member _XX of a client class
1404 # pylint: disable=W0212
1405 try:
1406 import cpplint
1407 import cpplint_chromium
1408 except ImportError:
1409 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1410 return 1
1411
1412 # Change the current working directory before calling lint so that it
1413 # shows the correct base.
1414 previous_cwd = os.getcwd()
1415 os.chdir(settings.GetRoot())
1416 try:
1417 cl = Changelist()
1418 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1419 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001420 if not files:
1421 print "Cannot lint an empty CL"
1422 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001423
1424 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001425 command = args + files
1426 if options.filter:
1427 command = ['--filter=' + ','.join(options.filter)] + command
1428 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001429
1430 white_regex = re.compile(settings.GetLintRegex())
1431 black_regex = re.compile(settings.GetLintIgnoreRegex())
1432 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1433 for filename in filenames:
1434 if white_regex.match(filename):
1435 if black_regex.match(filename):
1436 print "Ignoring file %s" % filename
1437 else:
1438 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1439 extra_check_functions)
1440 else:
1441 print "Skipping file %s" % filename
1442 finally:
1443 os.chdir(previous_cwd)
1444 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1445 if cpplint._cpplint_state.error_count != 0:
1446 return 1
1447 return 0
1448
1449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001451 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001452 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001454 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001455 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 (options, args) = parser.parse_args(args)
1457
ukai@chromium.org259e4682012-10-25 07:36:33 +00001458 if not options.force and is_dirty_git_tree('presubmit'):
1459 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 return 1
1461
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001462 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 if args:
1464 base_branch = args[0]
1465 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001466 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001467 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001469 cl.RunHook(
1470 committing=not options.upload,
1471 may_prompt=False,
1472 verbose=options.verbose,
1473 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001474 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
1476
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001477def AddChangeIdToCommitMessage(options, args):
1478 """Re-commits using the current message, assumes the commit hook is in
1479 place.
1480 """
1481 log_desc = options.message or CreateDescriptionFromLog(args)
1482 git_command = ['commit', '--amend', '-m', log_desc]
1483 RunGit(git_command)
1484 new_log_desc = CreateDescriptionFromLog(args)
1485 if CHANGE_ID in new_log_desc:
1486 print 'git-cl: Added Change-Id to commit message.'
1487 else:
1488 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1489
1490
ukai@chromium.orge8077812012-02-03 03:41:46 +00001491def GerritUpload(options, args, cl):
1492 """upload the current branch to gerrit."""
1493 # We assume the remote called "origin" is the one we want.
1494 # It is probably not worthwhile to support different workflows.
1495 remote = 'origin'
1496 branch = 'master'
1497 if options.target_branch:
1498 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001499
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001500 change_desc = ChangeDescription(
1501 options.message or CreateDescriptionFromLog(args))
1502 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001503 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001505 if CHANGE_ID not in change_desc.description:
1506 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001507
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001508 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001509 if len(commits) > 1:
1510 print('WARNING: This will upload %d commits. Run the following command '
1511 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001512 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001513 print('You can also use `git squash-branch` to squash these into a single'
1514 'commit.')
1515 ask_for_data('About to upload; enter to confirm.')
1516
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001517 if options.reviewers:
1518 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519
ukai@chromium.orge8077812012-02-03 03:41:46 +00001520 receive_options = []
1521 cc = cl.GetCCList().split(',')
1522 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001523 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001524 cc = filter(None, cc)
1525 if cc:
1526 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001527 if change_desc.get_reviewers():
1528 receive_options.extend(
1529 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530
ukai@chromium.orge8077812012-02-03 03:41:46 +00001531 git_command = ['push']
1532 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001533 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001534 ' '.join(receive_options))
1535 git_command += [remote, 'HEAD:refs/for/' + branch]
1536 RunGit(git_command)
1537 # TODO(ukai): parse Change-Id: and set issue number?
1538 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001539
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541def RietveldUpload(options, args, cl):
1542 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1544 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 if options.emulate_svn_auto_props:
1546 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547
1548 change_desc = None
1549
pgervais@chromium.org91141372014-01-09 23:27:20 +00001550 if options.email is not None:
1551 upload_args.extend(['--email', options.email])
1552
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001553 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001554 if options.title:
1555 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001556 if options.message:
1557 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001558 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559 print ("This branch is associated with issue %s. "
1560 "Adding patch to that issue." % cl.GetIssue())
1561 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001562 if options.title:
1563 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001564 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001565 change_desc = ChangeDescription(message)
1566 if options.reviewers:
1567 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001568 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001569 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001570
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001571 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572 print "Description is empty; aborting."
1573 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001574
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001575 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001576 if change_desc.get_reviewers():
1577 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001578 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001579 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001580 DieWithError("Must specify reviewers to send email.")
1581 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001582
1583 # We check this before applying rietveld.private assuming that in
1584 # rietveld.cc only addresses which we can send private CLs to are listed
1585 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1586 # --private is specified explicitly on the command line.
1587 if options.private:
1588 logging.warn('rietveld.cc is ignored since private flag is specified. '
1589 'You need to review and add them manually if necessary.')
1590 cc = cl.GetCCListWithoutDefault()
1591 else:
1592 cc = cl.GetCCList()
1593 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001594 if cc:
1595 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001597 if options.private or settings.GetDefaultPrivateFlag() == "True":
1598 upload_args.append('--private')
1599
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001600 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001601 if not options.find_copies:
1602 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001603
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 # Include the upstream repo's URL in the change -- this is useful for
1605 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001606 remote_url = cl.GetGitBaseUrlFromConfig()
1607 if not remote_url:
1608 if settings.GetIsGitSvn():
1609 # URL is dependent on the current directory.
1610 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1611 if data:
1612 keys = dict(line.split(': ', 1) for line in data.splitlines()
1613 if ': ' in line)
1614 remote_url = keys.get('URL', None)
1615 else:
1616 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1617 remote_url = (cl.GetRemoteUrl() + '@'
1618 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619 if remote_url:
1620 upload_args.extend(['--base_url', remote_url])
1621
1622 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001623 upload_args = ['upload'] + upload_args + args
1624 logging.info('upload.RealMain(%s)', upload_args)
1625 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001626 issue = int(issue)
1627 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001628 except KeyboardInterrupt:
1629 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630 except:
1631 # If we got an exception after the user typed a description for their
1632 # change, back up the description before re-raising.
1633 if change_desc:
1634 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1635 print '\nGot exception while uploading -- saving description to %s\n' \
1636 % backup_path
1637 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001638 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 backup_file.close()
1640 raise
1641
1642 if not cl.GetIssue():
1643 cl.SetIssue(issue)
1644 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001645
1646 if options.use_commit_queue:
1647 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001648 return 0
1649
1650
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001651def cleanup_list(l):
1652 """Fixes a list so that comma separated items are put as individual items.
1653
1654 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1655 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1656 """
1657 items = sum((i.split(',') for i in l), [])
1658 stripped_items = (i.strip() for i in items)
1659 return sorted(filter(None, stripped_items))
1660
1661
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001662@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001663def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001664 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001665 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1666 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001667 parser.add_option('--bypass-watchlists', action='store_true',
1668 dest='bypass_watchlists',
1669 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001670 parser.add_option('-f', action='store_true', dest='force',
1671 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001672 parser.add_option('-m', dest='message', help='message for patchset')
1673 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001674 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001675 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001676 help='reviewer email addresses')
1677 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001678 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001679 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001680 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001681 help='send email to reviewer immediately')
1682 parser.add_option("--emulate_svn_auto_props", action="store_true",
1683 dest="emulate_svn_auto_props",
1684 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001685 parser.add_option('-c', '--use-commit-queue', action='store_true',
1686 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001687 parser.add_option('--private', action='store_true',
1688 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001689 parser.add_option('--target_branch',
1690 help='When uploading to gerrit, remote branch to '
1691 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001692 parser.add_option('--email', default=None,
1693 help='email address to use to connect to Rietveld')
1694
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001695 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001696 (options, args) = parser.parse_args(args)
1697
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001698 if options.target_branch and not settings.GetIsGerrit():
1699 parser.error('Use --target_branch for non gerrit repository.')
1700
ukai@chromium.org259e4682012-10-25 07:36:33 +00001701 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001702 return 1
1703
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001704 options.reviewers = cleanup_list(options.reviewers)
1705 options.cc = cleanup_list(options.cc)
1706
ukai@chromium.orge8077812012-02-03 03:41:46 +00001707 cl = Changelist()
1708 if args:
1709 # TODO(ukai): is it ok for gerrit case?
1710 base_branch = args[0]
1711 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001712 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001713 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001714 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001715
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001716 # Apply watchlists on upload.
1717 change = cl.GetChange(base_branch, None)
1718 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1719 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001720 if not options.bypass_watchlists:
1721 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001722
ukai@chromium.orge8077812012-02-03 03:41:46 +00001723 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001724 if options.reviewers:
1725 # Set the reviewer list now so that presubmit checks can access it.
1726 change_description = ChangeDescription(change.FullDescriptionText())
1727 change_description.update_reviewers(options.reviewers)
1728 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001729 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001730 may_prompt=not options.force,
1731 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001732 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001733 if not hook_results.should_continue():
1734 return 1
1735 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001736 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001737
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001738 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001739 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001740 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001741 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001742 print ('The last upload made from this repository was patchset #%d but '
1743 'the most recent patchset on the server is #%d.'
1744 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001745 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1746 'from another machine or branch the patch you\'re uploading now '
1747 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001748 ask_for_data('About to upload; enter to confirm.')
1749
iannucci@chromium.org79540052012-10-19 23:15:26 +00001750 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001751 if settings.GetIsGerrit():
1752 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001753 ret = RietveldUpload(options, args, cl)
1754 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001755 git_set_branch_value('last-upload-hash',
1756 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001757
1758 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001759
1760
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001761def IsSubmoduleMergeCommit(ref):
1762 # When submodules are added to the repo, we expect there to be a single
1763 # non-git-svn merge commit at remote HEAD with a signature comment.
1764 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001765 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001766 return RunGit(cmd) != ''
1767
1768
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001770 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001772 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773 Updates changelog with metadata (e.g. pointer to review).
1774 Pushes/dcommits the code upstream.
1775 Updates review and closes.
1776 """
1777 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1778 help='bypass upload presubmit hook')
1779 parser.add_option('-m', dest='message',
1780 help="override review description")
1781 parser.add_option('-f', action='store_true', dest='force',
1782 help="force yes to questions (don't prompt)")
1783 parser.add_option('-c', dest='contributor',
1784 help="external contributor for patch (appended to " +
1785 "description and used as author for git). Should be " +
1786 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001787 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788 (options, args) = parser.parse_args(args)
1789 cl = Changelist()
1790
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001791 current = cl.GetBranch()
1792 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1793 if not settings.GetIsGitSvn() and remote == '.':
1794 print
1795 print 'Attempting to push branch %r into another local branch!' % current
1796 print
1797 print 'Either reparent this branch on top of origin/master:'
1798 print ' git reparent-branch --root'
1799 print
1800 print 'OR run `git rebase-update` if you think the parent branch is already'
1801 print 'committed.'
1802 print
1803 print ' Current parent: %r' % upstream_branch
1804 return 1
1805
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 if not args or cmd == 'push':
1807 # Default to merging against our best guess of the upstream branch.
1808 args = [cl.GetUpstreamBranch()]
1809
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001810 if options.contributor:
1811 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1812 print "Please provide contibutor as 'First Last <email@example.com>'"
1813 return 1
1814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001816 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817
ukai@chromium.org259e4682012-10-25 07:36:33 +00001818 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819 return 1
1820
1821 # This rev-list syntax means "show all commits not in my branch that
1822 # are in base_branch".
1823 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1824 base_branch]).splitlines()
1825 if upstream_commits:
1826 print ('Base branch "%s" has %d commits '
1827 'not in this branch.' % (base_branch, len(upstream_commits)))
1828 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1829 return 1
1830
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001831 # This is the revision `svn dcommit` will commit on top of.
1832 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1833 '--pretty=format:%H'])
1834
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001836 # If the base_head is a submodule merge commit, the first parent of the
1837 # base_head should be a git-svn commit, which is what we're interested in.
1838 base_svn_head = base_branch
1839 if base_has_submodules:
1840 base_svn_head += '^1'
1841
1842 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 if extra_commits:
1844 print ('This branch has %d additional commits not upstreamed yet.'
1845 % len(extra_commits.splitlines()))
1846 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1847 'before attempting to %s.' % (base_branch, cmd))
1848 return 1
1849
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001850 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001851 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001852 author = None
1853 if options.contributor:
1854 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001855 hook_results = cl.RunHook(
1856 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001857 may_prompt=not options.force,
1858 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001859 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001860 if not hook_results.should_continue():
1861 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001862
1863 if cmd == 'dcommit':
1864 # Check the tree status if the tree status URL is set.
1865 status = GetTreeStatus()
1866 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001867 print('The tree is closed. Please wait for it to reopen. Use '
1868 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001869 return 1
1870 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001871 print('Unable to determine tree status. Please verify manually and '
1872 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001873 else:
1874 breakpad.SendStack(
1875 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001876 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1877 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001878 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001879
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001880 change_desc = ChangeDescription(options.message)
1881 if not change_desc.description and cl.GetIssue():
1882 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001883
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001884 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001885 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001886 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001887 else:
1888 print 'No description set.'
1889 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1890 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001891
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001892 # Keep a separate copy for the commit message, because the commit message
1893 # contains the link to the Rietveld issue, while the Rietveld message contains
1894 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001895 # Keep a separate copy for the commit message.
1896 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001897 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001898
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001899 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001900 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001901 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001903 commit_desc.append_footer('Patch from %s.' % options.contributor)
1904
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001905 print('Description:')
1906 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907
1908 branches = [base_branch, cl.GetBranchRef()]
1909 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001910 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001911 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001913 # We want to squash all this branch's commits into one commit with the proper
1914 # description. We do this by doing a "reset --soft" to the base branch (which
1915 # keeps the working copy the same), then dcommitting that. If origin/master
1916 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1917 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001919 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1920 # Delete the branches if they exist.
1921 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1922 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1923 result = RunGitWithCode(showref_cmd)
1924 if result[0] == 0:
1925 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926
1927 # We might be in a directory that's present in this branch but not in the
1928 # trunk. Move up to the top of the tree so that git commands that expect a
1929 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001930 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931 if rel_base_path:
1932 os.chdir(rel_base_path)
1933
1934 # Stuff our change into the merge branch.
1935 # We wrap in a try...finally block so if anything goes wrong,
1936 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001937 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001939 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1940 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001942 RunGit(
1943 [
1944 'commit', '--author', options.contributor,
1945 '-m', commit_desc.description,
1946 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001948 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001949 if base_has_submodules:
1950 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1951 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1952 RunGit(['checkout', CHERRY_PICK_BRANCH])
1953 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001954 if cmd == 'push':
1955 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001956 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001957 retcode, output = RunGitWithCode(
1958 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1959 logging.debug(output)
1960 else:
1961 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001962 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001963 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001964 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 finally:
1966 # And then swap back to the original branch and clean up.
1967 RunGit(['checkout', '-q', cl.GetBranch()])
1968 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001969 if base_has_submodules:
1970 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971
1972 if cl.GetIssue():
1973 if cmd == 'dcommit' and 'Committed r' in output:
1974 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1975 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001976 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1977 for l in output.splitlines(False))
1978 match = filter(None, match)
1979 if len(match) != 1:
1980 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1981 output)
1982 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983 else:
1984 return 1
1985 viewvc_url = settings.GetViewVCUrl()
1986 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001987 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001988 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001989 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990 print ('Closing issue '
1991 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001992 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001994 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001995 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001996 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001997 if options.bypass_hooks:
1998 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1999 else:
2000 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002001 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002002 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002003
2004 if retcode == 0:
2005 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2006 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002007 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002008
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009 return 0
2010
2011
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002012@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002013def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002014 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002016 message = """This doesn't appear to be an SVN repository.
2017If your project has a git mirror with an upstream SVN master, you probably need
2018to run 'git svn init', see your project's git mirror documentation.
2019If your project has a true writeable upstream repository, you probably want
2020to run 'git cl push' instead.
2021Choose wisely, if you get this wrong, your commit might appear to succeed but
2022will instead be silently ignored."""
2023 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002024 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002025 return SendUpstream(parser, args, 'dcommit')
2026
2027
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002028@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002029def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002030 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002031 if settings.GetIsGitSvn():
2032 print('This appears to be an SVN repository.')
2033 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002034 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002035 return SendUpstream(parser, args, 'push')
2036
2037
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002038@subcommand.usage('[upstream branch to apply against]')
2039def CMDpush(parser, args):
2040 """Temporary alias for 'land'.
2041 """
2042 print(
2043 "\n=======\n"
2044 "'git cl push' has been renamed to 'git cl land'.\n"
2045 "Currently they are treated as synonyms, but 'git cl push' will stop\n"
2046 "working after 2014/07/01.\n"
2047 "=======\n")
2048 now = datetime.datetime.utcfromtimestamp(time.time())
2049 if now > datetime.datetime(2014, 7, 1):
2050 print('******\nReally, you should not use this command anymore... \n'
2051 'Pausing 10 sec to help you remember :-)')
2052 for n in xrange(10):
2053 time.sleep(1)
2054 print('%s seconds...' % (n+1))
2055 print('******')
2056 return CMDland(parser, args)
2057
2058
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002059@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002061 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062 parser.add_option('-b', dest='newbranch',
2063 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002064 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002066 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2067 help='Change to the directory DIR immediately, '
2068 'before doing anything else.')
2069 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002070 help='failed patches spew .rej files rather than '
2071 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2073 help="don't commit after patch applies")
2074 (options, args) = parser.parse_args(args)
2075 if len(args) != 1:
2076 parser.print_help()
2077 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002078 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002080 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002081 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002082
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002083 if options.newbranch:
2084 if options.force:
2085 RunGit(['branch', '-D', options.newbranch],
2086 stderr=subprocess2.PIPE, error_ok=True)
2087 RunGit(['checkout', '-b', options.newbranch,
2088 Changelist().GetUpstreamBranch()])
2089
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002090 return PatchIssue(issue_arg, options.reject, options.nocommit,
2091 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002092
2093
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002094def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002095 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002097 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002098 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002099 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002100 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002102 # Assume it's a URL to the patch. Default to https.
2103 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002104 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002105 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 DieWithError('Must pass an issue ID or full URL for '
2107 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002108 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002109 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002110 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002112 # Switch up to the top-level directory, if necessary, in preparation for
2113 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002114 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115 if top:
2116 os.chdir(top)
2117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 # Git patches have a/ at the beginning of source paths. We strip that out
2119 # with a sed script rather than the -p flag to patch so we can feed either
2120 # Git or svn-style patches into the same apply command.
2121 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002122 try:
2123 patch_data = subprocess2.check_output(
2124 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2125 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126 DieWithError('Git patch mungling failed.')
2127 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129 # We use "git apply" to apply the patch instead of "patch" so that we can
2130 # pick up file adds.
2131 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002132 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002133 if directory:
2134 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002135 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002137 elif IsGitVersionAtLeast('1.7.12'):
2138 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002139 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002140 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002141 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002142 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002143 DieWithError('Failed to apply the patch')
2144
2145 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002146 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2148 cl = Changelist()
2149 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002150 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002151 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 else:
2153 print "Patch applied to index."
2154 return 0
2155
2156
2157def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002158 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002159 # Provide a wrapper for git svn rebase to help avoid accidental
2160 # git svn dcommit.
2161 # It's the only command that doesn't use parser at all since we just defer
2162 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002163
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002164 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165
2166
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002167def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002168 """Fetches the tree status and returns either 'open', 'closed',
2169 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002170 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002171 if url:
2172 status = urllib2.urlopen(url).read().lower()
2173 if status.find('closed') != -1 or status == '0':
2174 return 'closed'
2175 elif status.find('open') != -1 or status == '1':
2176 return 'open'
2177 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002178 return 'unset'
2179
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002181def GetTreeStatusReason():
2182 """Fetches the tree status from a json url and returns the message
2183 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002184 url = settings.GetTreeStatusUrl()
2185 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002186 connection = urllib2.urlopen(json_url)
2187 status = json.loads(connection.read())
2188 connection.close()
2189 return status['message']
2190
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002193 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002194 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195 status = GetTreeStatus()
2196 if 'unset' == status:
2197 print 'You must configure your tree status URL by running "git cl config".'
2198 return 2
2199
2200 print "The tree is %s" % status
2201 print
2202 print GetTreeStatusReason()
2203 if status != 'open':
2204 return 1
2205 return 0
2206
2207
maruel@chromium.org15192402012-09-06 12:38:29 +00002208def CMDtry(parser, args):
2209 """Triggers a try job through Rietveld."""
2210 group = optparse.OptionGroup(parser, "Try job options")
2211 group.add_option(
2212 "-b", "--bot", action="append",
2213 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2214 "times to specify multiple builders. ex: "
2215 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2216 "the try server waterfall for the builders name and the tests "
2217 "available. Can also be used to specify gtest_filter, e.g. "
2218 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2219 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002220 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002221 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002222 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002223 "-r", "--revision",
2224 help="Revision to use for the try job; default: the "
2225 "revision will be determined by the try server; see "
2226 "its waterfall for more info")
2227 group.add_option(
2228 "-c", "--clobber", action="store_true", default=False,
2229 help="Force a clobber before building; e.g. don't do an "
2230 "incremental build")
2231 group.add_option(
2232 "--project",
2233 help="Override which project to use. Projects are defined "
2234 "server-side to define what default bot set to use")
2235 group.add_option(
2236 "-t", "--testfilter", action="append", default=[],
2237 help=("Apply a testfilter to all the selected builders. Unless the "
2238 "builders configurations are similar, use multiple "
2239 "--bot <builder>:<test> arguments."))
2240 group.add_option(
2241 "-n", "--name", help="Try job name; default to current branch name")
2242 parser.add_option_group(group)
2243 options, args = parser.parse_args(args)
2244
2245 if args:
2246 parser.error('Unknown arguments: %s' % args)
2247
2248 cl = Changelist()
2249 if not cl.GetIssue():
2250 parser.error('Need to upload first')
2251
2252 if not options.name:
2253 options.name = cl.GetBranch()
2254
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002255 if options.bot and not options.master:
2256 parser.error('For manually specified bots please also specify the '
2257 'tryserver master, e.g. "-m tryserver.chromium".')
2258
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002259 def GetMasterMap():
2260 # Process --bot and --testfilter.
2261 if not options.bot:
2262 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002263
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002264 # Get try masters from PRESUBMIT.py files.
2265 masters = presubmit_support.DoGetTryMasters(
2266 change,
2267 change.LocalPaths(),
2268 settings.GetRoot(),
2269 None,
2270 None,
2271 options.verbose,
2272 sys.stdout)
2273 if masters:
2274 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002275
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002276 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2277 options.bot = presubmit_support.DoGetTrySlaves(
2278 change,
2279 change.LocalPaths(),
2280 settings.GetRoot(),
2281 None,
2282 None,
2283 options.verbose,
2284 sys.stdout)
2285 if not options.bot:
2286 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002287
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002288 builders_and_tests = {}
2289 # TODO(machenbach): The old style command-line options don't support
2290 # multiple try masters yet.
2291 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2292 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2293
2294 for bot in old_style:
2295 if ':' in bot:
2296 builder, tests = bot.split(':', 1)
2297 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2298 elif ',' in bot:
2299 parser.error('Specify one bot per --bot flag')
2300 else:
2301 builders_and_tests.setdefault(bot, []).append('defaulttests')
2302
2303 for bot, tests in new_style:
2304 builders_and_tests.setdefault(bot, []).extend(tests)
2305
2306 # Return a master map with one master to be backwards compatible. The
2307 # master name defaults to an empty string, which will cause the master
2308 # not to be set on rietveld (deprecated).
2309 return {options.master: builders_and_tests}
2310
2311 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002312
maruel@chromium.org15192402012-09-06 12:38:29 +00002313 if options.testfilter:
2314 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002315 masters = dict((master, dict(
2316 (b, forced_tests) for b, t in slaves.iteritems()
2317 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002318
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002319 for builders in masters.itervalues():
2320 if any('triggered' in b for b in builders):
2321 print >> sys.stderr, (
2322 'ERROR You are trying to send a job to a triggered bot. This type of'
2323 ' bot requires an\ninitial job from a parent (usually a builder). '
2324 'Instead send your job to the parent.\n'
2325 'Bot list: %s' % builders)
2326 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002327
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002328 patchset = cl.GetMostRecentPatchset()
2329 if patchset and patchset != cl.GetPatchset():
2330 print(
2331 '\nWARNING Mismatch between local config and server. Did a previous '
2332 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2333 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002334 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002335 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002336 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002337 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002338 except urllib2.HTTPError, e:
2339 if e.code == 404:
2340 print('404 from rietveld; '
2341 'did you mean to use "git try" instead of "git cl try"?')
2342 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002343 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002344
2345 for (master, builders) in masters.iteritems():
2346 if master:
2347 print 'Master: %s' % master
2348 length = max(len(builder) for builder in builders)
2349 for builder in sorted(builders):
2350 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002351 return 0
2352
2353
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002354@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002355def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002356 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002357 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002358 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002359 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002360
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002361 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002362 if args:
2363 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002364 branch = cl.GetBranch()
2365 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002366 cl = Changelist()
2367 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002368
2369 # Clear configured merge-base, if there is one.
2370 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002371 else:
2372 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002373 return 0
2374
2375
thestig@chromium.org00858c82013-12-02 23:08:03 +00002376def CMDweb(parser, args):
2377 """Opens the current CL in the web browser."""
2378 _, args = parser.parse_args(args)
2379 if args:
2380 parser.error('Unrecognized args: %s' % ' '.join(args))
2381
2382 issue_url = Changelist().GetIssueURL()
2383 if not issue_url:
2384 print >> sys.stderr, 'ERROR No issue to open'
2385 return 1
2386
2387 webbrowser.open(issue_url)
2388 return 0
2389
2390
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002391def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002392 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002393 _, args = parser.parse_args(args)
2394 if args:
2395 parser.error('Unrecognized args: %s' % ' '.join(args))
2396 cl = Changelist()
2397 cl.SetFlag('commit', '1')
2398 return 0
2399
2400
groby@chromium.org411034a2013-02-26 15:12:01 +00002401def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002402 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002403 _, args = parser.parse_args(args)
2404 if args:
2405 parser.error('Unrecognized args: %s' % ' '.join(args))
2406 cl = Changelist()
2407 # Ensure there actually is an issue to close.
2408 cl.GetDescription()
2409 cl.CloseIssue()
2410 return 0
2411
2412
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002413def CMDdiff(parser, args):
2414 """shows differences between local tree and last upload."""
2415 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002416 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002417 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002418 if not issue:
2419 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002420 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002421 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002422
2423 # Create a new branch based on the merge-base
2424 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2425 try:
2426 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002427 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002428 if rtn != 0:
2429 return rtn
2430
2431 # Switch back to starting brand and diff against the temporary
2432 # branch containing the latest rietveld patch.
2433 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2434 finally:
2435 RunGit(['checkout', '-q', branch])
2436 RunGit(['branch', '-D', TMP_BRANCH])
2437
2438 return 0
2439
2440
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002441def CMDowners(parser, args):
2442 """interactively find the owners for reviewing"""
2443 parser.add_option(
2444 '--no-color',
2445 action='store_true',
2446 help='Use this option to disable color output')
2447 options, args = parser.parse_args(args)
2448
2449 author = RunGit(['config', 'user.email']).strip() or None
2450
2451 cl = Changelist()
2452
2453 if args:
2454 if len(args) > 1:
2455 parser.error('Unknown args')
2456 base_branch = args[0]
2457 else:
2458 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002459 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002460
2461 change = cl.GetChange(base_branch, None)
2462 return owners_finder.OwnersFinder(
2463 [f.LocalPath() for f in
2464 cl.GetChange(base_branch, None).AffectedFiles()],
2465 change.RepositoryRoot(), author,
2466 fopen=file, os_path=os.path, glob=glob.glob,
2467 disable_color=options.no_color).run()
2468
2469
enne@chromium.org555cfe42014-01-29 18:21:39 +00002470@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002471def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002472 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002473 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002474 parser.add_option('--full', action='store_true',
2475 help='Reformat the full content of all touched files')
2476 parser.add_option('--dry-run', action='store_true',
2477 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002478 parser.add_option('--diff', action='store_true',
2479 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002480 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002481
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002482 # git diff generates paths against the root of the repository. Change
2483 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002484 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002485 if rel_base_path:
2486 os.chdir(rel_base_path)
2487
digit@chromium.org29e47272013-05-17 17:01:46 +00002488 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002489 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002490 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002491 # Only list the names of modified files.
2492 diff_cmd.append('--name-only')
2493 else:
2494 # Only generate context-less patches.
2495 diff_cmd.append('-U0')
2496
2497 # Grab the merge-base commit, i.e. the upstream commit of the current
2498 # branch when it was created or the last time it was rebased. This is
2499 # to cover the case where the user may have called "git fetch origin",
2500 # moving the origin branch to a newer commit, but hasn't rebased yet.
2501 upstream_commit = None
2502 cl = Changelist()
2503 upstream_branch = cl.GetUpstreamBranch()
2504 if upstream_branch:
2505 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2506 upstream_commit = upstream_commit.strip()
2507
2508 if not upstream_commit:
2509 DieWithError('Could not find base commit for this branch. '
2510 'Are you in detached state?')
2511
2512 diff_cmd.append(upstream_commit)
2513
2514 # Handle source file filtering.
2515 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002516 if args:
2517 for arg in args:
2518 if os.path.isdir(arg):
2519 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2520 elif os.path.isfile(arg):
2521 diff_cmd.append(arg)
2522 else:
2523 DieWithError('Argument "%s" is not a file or a directory' % arg)
2524 else:
2525 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002526 diff_output = RunGit(diff_cmd)
2527
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002528 top_dir = os.path.normpath(
2529 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2530
2531 # Locate the clang-format binary in the checkout
2532 try:
2533 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2534 except clang_format.NotFoundError, e:
2535 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002536
digit@chromium.org29e47272013-05-17 17:01:46 +00002537 if opts.full:
2538 # diff_output is a list of files to send to clang-format.
2539 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002540 if not files:
2541 print "Nothing to format."
2542 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002543 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002544 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002545 cmd.append('-i')
2546 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002547 if opts.diff:
2548 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002549 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002550 env = os.environ.copy()
2551 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002552 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002553 try:
2554 script = clang_format.FindClangFormatScriptInChromiumTree(
2555 'clang-format-diff.py')
2556 except clang_format.NotFoundError, e:
2557 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002558
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002559 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002560 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002561 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002562
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002563 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002564 if opts.diff:
2565 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002566 if opts.dry_run and len(stdout) > 0:
2567 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002568
2569 return 0
2570
2571
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002572class OptionParser(optparse.OptionParser):
2573 """Creates the option parse and add --verbose support."""
2574 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002575 optparse.OptionParser.__init__(
2576 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002577 self.add_option(
2578 '-v', '--verbose', action='count', default=0,
2579 help='Use 2 times for more debugging info')
2580
2581 def parse_args(self, args=None, values=None):
2582 options, args = optparse.OptionParser.parse_args(self, args, values)
2583 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2584 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2585 return options, args
2586
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002587
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002588def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002589 if sys.hexversion < 0x02060000:
2590 print >> sys.stderr, (
2591 '\nYour python version %s is unsupported, please upgrade.\n' %
2592 sys.version.split(' ', 1)[0])
2593 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002594
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002595 # Reload settings.
2596 global settings
2597 settings = Settings()
2598
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002599 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002600 dispatcher = subcommand.CommandDispatcher(__name__)
2601 try:
2602 return dispatcher.execute(OptionParser(), argv)
2603 except urllib2.HTTPError, e:
2604 if e.code != 500:
2605 raise
2606 DieWithError(
2607 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2608 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002609
2610
2611if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002612 # These affect sys.stdout so do it outside of main() to simplify mocks in
2613 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002614 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002615 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002616 sys.exit(main(sys.argv[1:]))