blob: c8c83309f4c47ac8decd1e698c67a1cf125d29b6 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000010import datetime
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000013import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import logging
15import optparse
16import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000017import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import textwrap
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000022import time
maruel@chromium.org1033efd2013-07-23 23:25:09 +000023import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000025import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000026import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027
28try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000029 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030except ImportError:
31 pass
32
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035from third_party import upload
36import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
41import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000043import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000046import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047import watchlists
48
maruel@chromium.org0633fb42013-08-16 20:06:14 +000049__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000051DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000052POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000054GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000055CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056
thestig@chromium.org44202a22014-03-11 19:22:18 +000057# Valid extensions for files we want to lint.
58DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
59DEFAULT_LINT_IGNORE_REGEX = r"$^"
60
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000061# Shortcut since it quickly becomes redundant.
62Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000063
maruel@chromium.orgddd59412011-11-30 14:20:38 +000064# Initialized in main()
65settings = None
66
67
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000069 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070 sys.exit(1)
71
72
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000073def GetNoGitPagerEnv():
74 env = os.environ.copy()
75 # 'cat' is a magical git string that disables pagers on all platforms.
76 env['GIT_PAGER'] = 'cat'
77 return env
78
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000081 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000082 except subprocess2.CalledProcessError as e:
83 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000086 'Command "%s" failed.\n%s' % (
87 ' '.join(args), error_message or e.stdout or ''))
88 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089
90
91def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000092 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000093 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000094
95
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000096def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000098 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000099 if suppress_stderr:
100 stderr = subprocess2.VOID
101 else:
102 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000103 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000104 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000105 stdout=subprocess2.PIPE,
106 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000107 return code, out[0]
108 except ValueError:
109 # When the subprocess fails, it returns None. That triggers a ValueError
110 # when trying to unpack the return value into (out, code).
111 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000114def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000115 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000117 return (version.startswith(prefix) and
118 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000119
120
maruel@chromium.org90541732011-04-01 17:54:18 +0000121def ask_for_data(prompt):
122 try:
123 return raw_input(prompt)
124 except KeyboardInterrupt:
125 # Hide the exception.
126 sys.exit(1)
127
128
iannucci@chromium.org79540052012-10-19 23:15:26 +0000129def git_set_branch_value(key, value):
130 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000131 if not branch:
132 return
133
134 cmd = ['config']
135 if isinstance(value, int):
136 cmd.append('--int')
137 git_key = 'branch.%s.%s' % (branch, key)
138 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139
140
141def git_get_branch_default(key, default):
142 branch = Changelist().GetBranch()
143 if branch:
144 git_key = 'branch.%s.%s' % (branch, key)
145 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
146 try:
147 return int(stdout.strip())
148 except ValueError:
149 pass
150 return default
151
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153def add_git_similarity(parser):
154 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 help='Sets the percentage that a pair of files need to match in order to'
157 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158 parser.add_option(
159 '--find-copies', action='store_true',
160 help='Allows git to look for copies.')
161 parser.add_option(
162 '--no-find-copies', action='store_false', dest='find_copies',
163 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164
165 old_parser_args = parser.parse_args
166 def Parse(args):
167 options, args = old_parser_args(args)
168
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000171 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 print('Note: Saving similarity of %d%% in git config.'
173 % options.similarity)
174 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000175
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 options.similarity = max(0, min(options.similarity, 100))
177
178 if options.find_copies is None:
179 options.find_copies = bool(
180 git_get_branch_default('git-find-copies', True))
181 else:
182 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183
184 print('Using %d%% similarity for rename/copy detection. '
185 'Override with --similarity.' % options.similarity)
186
187 return options, args
188 parser.parse_args = Parse
189
190
ukai@chromium.org259e4682012-10-25 07:36:33 +0000191def is_dirty_git_tree(cmd):
192 # Make sure index is up-to-date before running diff-index.
193 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
194 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
195 if dirty:
196 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
197 print 'Uncommitted files: (git diff-index --name-status HEAD)'
198 print dirty[:4096]
199 if len(dirty) > 4096:
200 print '... (run "git diff-index --name-status HEAD" to see full output).'
201 return True
202 return False
203
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000204
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000205def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
206 """Return the corresponding git ref if |base_url| together with |glob_spec|
207 matches the full |url|.
208
209 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
210 """
211 fetch_suburl, as_ref = glob_spec.split(':')
212 if allow_wildcards:
213 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
214 if glob_match:
215 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
216 # "branches/{472,597,648}/src:refs/remotes/svn/*".
217 branch_re = re.escape(base_url)
218 if glob_match.group(1):
219 branch_re += '/' + re.escape(glob_match.group(1))
220 wildcard = glob_match.group(2)
221 if wildcard == '*':
222 branch_re += '([^/]*)'
223 else:
224 # Escape and replace surrounding braces with parentheses and commas
225 # with pipe symbols.
226 wildcard = re.escape(wildcard)
227 wildcard = re.sub('^\\\\{', '(', wildcard)
228 wildcard = re.sub('\\\\,', '|', wildcard)
229 wildcard = re.sub('\\\\}$', ')', wildcard)
230 branch_re += wildcard
231 if glob_match.group(3):
232 branch_re += re.escape(glob_match.group(3))
233 match = re.match(branch_re, url)
234 if match:
235 return re.sub('\*$', match.group(1), as_ref)
236
237 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
238 if fetch_suburl:
239 full_url = base_url + '/' + fetch_suburl
240 else:
241 full_url = base_url
242 if full_url == url:
243 return as_ref
244 return None
245
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000246
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 """Prints statistics about the change to the user."""
249 # --no-ext-diff is broken in some versions of Git, so try to work around
250 # this by overriding the environment (but there is still a problem if the
251 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000252 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000253 if 'GIT_EXTERNAL_DIFF' in env:
254 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255
256 if find_copies:
257 similarity_options = ['--find-copies-harder', '-l100000',
258 '-C%s' % similarity]
259 else:
260 similarity_options = ['-M%s' % similarity]
261
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000262 try:
263 stdout = sys.stdout.fileno()
264 except AttributeError:
265 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000266 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000267 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000268 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000269 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000270
271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272class Settings(object):
273 def __init__(self):
274 self.default_server = None
275 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000276 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 self.is_git_svn = None
278 self.svn_branch = None
279 self.tree_status_url = None
280 self.viewvc_url = None
281 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000282 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000283 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000284 self.project = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285
286 def LazyUpdateIfNeeded(self):
287 """Updates the settings from a codereview.settings file, if available."""
288 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000289 # The only value that actually changes the behavior is
290 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000291 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000292 error_ok=True
293 ).strip().lower()
294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000296 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000298 # set updated to True to avoid infinite calling loop
299 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000300 self.updated = True
301 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 self.updated = True
303
304 def GetDefaultServerUrl(self, error_ok=False):
305 if not self.default_server:
306 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000307 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000308 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309 if error_ok:
310 return self.default_server
311 if not self.default_server:
312 error_message = ('Could not find settings file. You must configure '
313 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000314 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000315 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316 return self.default_server
317
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000318 @staticmethod
319 def GetRelativeRoot():
320 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000323 if self.root is None:
324 self.root = os.path.abspath(self.GetRelativeRoot())
325 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326
327 def GetIsGitSvn(self):
328 """Return true if this repo looks like it's using git-svn."""
329 if self.is_git_svn is None:
330 # If you have any "svn-remote.*" config keys, we think you're using svn.
331 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000332 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333 return self.is_git_svn
334
335 def GetSVNBranch(self):
336 if self.svn_branch is None:
337 if not self.GetIsGitSvn():
338 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
339
340 # Try to figure out which remote branch we're based on.
341 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000342 # 1) iterate through our branch history and find the svn URL.
343 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000344
345 # regexp matching the git-svn line that contains the URL.
346 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
347
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000348 # We don't want to go through all of history, so read a line from the
349 # pipe at a time.
350 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000351 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000352 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
353 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000354 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000355 for line in proc.stdout:
356 match = git_svn_re.match(line)
357 if match:
358 url = match.group(1)
359 proc.stdout.close() # Cut pipe.
360 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000361
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 if url:
363 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
364 remotes = RunGit(['config', '--get-regexp',
365 r'^svn-remote\..*\.url']).splitlines()
366 for remote in remotes:
367 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000368 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000369 remote = match.group(1)
370 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000371 rewrite_root = RunGit(
372 ['config', 'svn-remote.%s.rewriteRoot' % remote],
373 error_ok=True).strip()
374 if rewrite_root:
375 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000376 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000377 ['config', 'svn-remote.%s.fetch' % remote],
378 error_ok=True).strip()
379 if fetch_spec:
380 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
381 if self.svn_branch:
382 break
383 branch_spec = RunGit(
384 ['config', 'svn-remote.%s.branches' % remote],
385 error_ok=True).strip()
386 if branch_spec:
387 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
388 if self.svn_branch:
389 break
390 tag_spec = RunGit(
391 ['config', 'svn-remote.%s.tags' % remote],
392 error_ok=True).strip()
393 if tag_spec:
394 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
395 if self.svn_branch:
396 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000397
398 if not self.svn_branch:
399 DieWithError('Can\'t guess svn branch -- try specifying it on the '
400 'command line')
401
402 return self.svn_branch
403
404 def GetTreeStatusUrl(self, error_ok=False):
405 if not self.tree_status_url:
406 error_message = ('You must configure your tree status URL by running '
407 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000408 self.tree_status_url = self._GetRietveldConfig(
409 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000410 return self.tree_status_url
411
412 def GetViewVCUrl(self):
413 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000414 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 return self.viewvc_url
416
rmistry@google.com90752582014-01-14 21:04:50 +0000417 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000418 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000419
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000420 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000421 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000422
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000423 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000424 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000425
ukai@chromium.orge8077812012-02-03 03:41:46 +0000426 def GetIsGerrit(self):
427 """Return true if this repo is assosiated with gerrit code review system."""
428 if self.is_gerrit is None:
429 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
430 return self.is_gerrit
431
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000432 def GetGitEditor(self):
433 """Return the editor specified in the git config, or None if none is."""
434 if self.git_editor is None:
435 self.git_editor = self._GetConfig('core.editor', error_ok=True)
436 return self.git_editor or None
437
thestig@chromium.org44202a22014-03-11 19:22:18 +0000438 def GetLintRegex(self):
439 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
440 DEFAULT_LINT_REGEX)
441
442 def GetLintIgnoreRegex(self):
443 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
444 DEFAULT_LINT_IGNORE_REGEX)
445
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000446 def GetProject(self):
447 if not self.project:
448 self.project = self._GetRietveldConfig('project', error_ok=True)
449 return self.project
450
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000451 def _GetRietveldConfig(self, param, **kwargs):
452 return self._GetConfig('rietveld.' + param, **kwargs)
453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000454 def _GetConfig(self, param, **kwargs):
455 self.LazyUpdateIfNeeded()
456 return RunGit(['config', param], **kwargs).strip()
457
458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459def ShortBranchName(branch):
460 """Convert a name like 'refs/heads/foo' to just 'foo'."""
461 return branch.replace('refs/heads/', '')
462
463
464class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000465 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000467 global settings
468 if not settings:
469 # Happens when git_cl.py is used as a utility library.
470 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 settings.GetDefaultServerUrl()
472 self.branchref = branchref
473 if self.branchref:
474 self.branch = ShortBranchName(self.branchref)
475 else:
476 self.branch = None
477 self.rietveld_server = None
478 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000479 self.lookedup_issue = False
480 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 self.has_description = False
482 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000483 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000485 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000486 self.cc = None
487 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000488 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000489 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000490
491 def GetCCList(self):
492 """Return the users cc'd on this CL.
493
494 Return is a string suitable for passing to gcl with the --cc flag.
495 """
496 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000497 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000498 more_cc = ','.join(self.watchers)
499 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
500 return self.cc
501
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000502 def GetCCListWithoutDefault(self):
503 """Return the users cc'd on this CL excluding default ones."""
504 if self.cc is None:
505 self.cc = ','.join(self.watchers)
506 return self.cc
507
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000508 def SetWatchers(self, watchers):
509 """Set the list of email addresses that should be cc'd based on the changed
510 files in this CL.
511 """
512 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000513
514 def GetBranch(self):
515 """Returns the short branch name, e.g. 'master'."""
516 if not self.branch:
517 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
518 self.branch = ShortBranchName(self.branchref)
519 return self.branch
520
521 def GetBranchRef(self):
522 """Returns the full branch name, e.g. 'refs/heads/master'."""
523 self.GetBranch() # Poke the lazy loader.
524 return self.branchref
525
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000526 @staticmethod
527 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000528 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000529 e.g. 'origin', 'refs/heads/master'
530 """
531 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000532 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
533 error_ok=True).strip()
534 if upstream_branch:
535 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
536 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000537 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
538 error_ok=True).strip()
539 if upstream_branch:
540 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000541 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000542 # Fall back on trying a git-svn upstream branch.
543 if settings.GetIsGitSvn():
544 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000545 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000546 # Else, try to guess the origin remote.
547 remote_branches = RunGit(['branch', '-r']).split()
548 if 'origin/master' in remote_branches:
549 # Fall back on origin/master if it exits.
550 remote = 'origin'
551 upstream_branch = 'refs/heads/master'
552 elif 'origin/trunk' in remote_branches:
553 # Fall back on origin/trunk if it exists. Generally a shared
554 # git-svn clone
555 remote = 'origin'
556 upstream_branch = 'refs/heads/trunk'
557 else:
558 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000559Either pass complete "git diff"-style arguments, like
560 git cl upload origin/master
561or verify this branch is set up to track another (via the --track argument to
562"git checkout -b ...").""")
563
564 return remote, upstream_branch
565
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000566 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000567 return git_common.get_or_create_merge_base(self.GetBranch(),
568 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000569
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 def GetUpstreamBranch(self):
571 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000572 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573 if remote is not '.':
574 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
575 self.upstream_branch = upstream_branch
576 return self.upstream_branch
577
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000578 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000579 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000580 remote, branch = None, self.GetBranch()
581 seen_branches = set()
582 while branch not in seen_branches:
583 seen_branches.add(branch)
584 remote, branch = self.FetchUpstreamTuple(branch)
585 branch = ShortBranchName(branch)
586 if remote != '.' or branch.startswith('refs/remotes'):
587 break
588 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000589 remotes = RunGit(['remote'], error_ok=True).split()
590 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000591 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000592 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000593 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000594 logging.warning('Could not determine which remote this change is '
595 'associated with, so defaulting to "%s". This may '
596 'not be what you want. You may prevent this message '
597 'by running "git svn info" as documented here: %s',
598 self._remote,
599 GIT_INSTRUCTIONS_URL)
600 else:
601 logging.warn('Could not determine which remote this change is '
602 'associated with. You may prevent this message by '
603 'running "git svn info" as documented here: %s',
604 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000605 branch = 'HEAD'
606 if branch.startswith('refs/remotes'):
607 self._remote = (remote, branch)
608 else:
609 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000610 return self._remote
611
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000612 def GitSanityChecks(self, upstream_git_obj):
613 """Checks git repo status and ensures diff is from local commits."""
614
615 # Verify the commit we're diffing against is in our current branch.
616 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
617 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
618 if upstream_sha != common_ancestor:
619 print >> sys.stderr, (
620 'ERROR: %s is not in the current branch. You may need to rebase '
621 'your tracking branch' % upstream_sha)
622 return False
623
624 # List the commits inside the diff, and verify they are all local.
625 commits_in_diff = RunGit(
626 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
627 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
628 remote_branch = remote_branch.strip()
629 if code != 0:
630 _, remote_branch = self.GetRemoteBranch()
631
632 commits_in_remote = RunGit(
633 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
634
635 common_commits = set(commits_in_diff) & set(commits_in_remote)
636 if common_commits:
637 print >> sys.stderr, (
638 'ERROR: Your diff contains %d commits already in %s.\n'
639 'Run "git log --oneline %s..HEAD" to get a list of commits in '
640 'the diff. If you are using a custom git flow, you can override'
641 ' the reference used for this check with "git config '
642 'gitcl.remotebranch <git-ref>".' % (
643 len(common_commits), remote_branch, upstream_git_obj))
644 return False
645 return True
646
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000647 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000648 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000649
650 Returns None if it is not set.
651 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000652 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
653 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000654
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 def GetRemoteUrl(self):
656 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
657
658 Returns None if there is no remote.
659 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000660 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
662
663 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000664 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000665 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000667 self.issue = int(issue) or None if issue else None
668 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 return self.issue
670
671 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000672 if not self.rietveld_server:
673 # If we're on a branch then get the server potentially associated
674 # with that branch.
675 if self.GetIssue():
676 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
677 ['config', self._RietveldServer()], error_ok=True).strip())
678 if not self.rietveld_server:
679 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 return self.rietveld_server
681
682 def GetIssueURL(self):
683 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000684 if not self.GetIssue():
685 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
687
688 def GetDescription(self, pretty=False):
689 if not self.has_description:
690 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000691 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000692 try:
693 self.description = self.RpcServer().get_description(issue).strip()
694 except urllib2.HTTPError, e:
695 if e.code == 404:
696 DieWithError(
697 ('\nWhile fetching the description for issue %d, received a '
698 '404 (not found)\n'
699 'error. It is likely that you deleted this '
700 'issue on the server. If this is the\n'
701 'case, please run\n\n'
702 ' git cl issue 0\n\n'
703 'to clear the association with the deleted issue. Then run '
704 'this command again.') % issue)
705 else:
706 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000707 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 self.has_description = True
709 if pretty:
710 wrapper = textwrap.TextWrapper()
711 wrapper.initial_indent = wrapper.subsequent_indent = ' '
712 return wrapper.fill(self.description)
713 return self.description
714
715 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000716 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000717 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 patchset = RunGit(['config', self._PatchsetSetting()],
719 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000720 self.patchset = int(patchset) or None if patchset else None
721 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 return self.patchset
723
724 def SetPatchset(self, patchset):
725 """Set this branch's patchset. If patchset=0, clears the patchset."""
726 if patchset:
727 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000728 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 else:
730 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000731 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000732 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000734 def GetMostRecentPatchset(self):
735 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000736
737 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000738 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000739 '/download/issue%s_%s.diff' % (issue, patchset))
740
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000741 def GetIssueProperties(self):
742 if self._props is None:
743 issue = self.GetIssue()
744 if not issue:
745 self._props = {}
746 else:
747 self._props = self.RpcServer().get_issue_properties(issue, True)
748 return self._props
749
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000750 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000751 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000752
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 def SetIssue(self, issue):
754 """Set this branch's issue. If issue=0, clears the issue."""
755 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000756 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757 RunGit(['config', self._IssueSetting(), str(issue)])
758 if self.rietveld_server:
759 RunGit(['config', self._RietveldServer(), self.rietveld_server])
760 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000761 current_issue = self.GetIssue()
762 if current_issue:
763 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000764 self.issue = None
765 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000767 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000768 if not self.GitSanityChecks(upstream_branch):
769 DieWithError('\nGit sanity check failure')
770
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000771 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000772 if not root:
773 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000774 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000775
776 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000777 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000778 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000779 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000780 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000781 except subprocess2.CalledProcessError:
782 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000783 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000784 'This branch probably doesn\'t exist anymore. To reset the\n'
785 'tracking branch, please run\n'
786 ' git branch --set-upstream %s trunk\n'
787 'replacing trunk with origin/master or the relevant branch') %
788 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000789
maruel@chromium.org52424302012-08-29 15:14:30 +0000790 issue = self.GetIssue()
791 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000792 if issue:
793 description = self.GetDescription()
794 else:
795 # If the change was never uploaded, use the log messages of all commits
796 # up to the branch point, as git cl upload will prefill the description
797 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000798 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
799 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000800
801 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000802 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000803 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000804 name,
805 description,
806 absroot,
807 files,
808 issue,
809 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000810 author,
811 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000812
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000813 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000814 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000815
816 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000817 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000818 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000819 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000820 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000821 except presubmit_support.PresubmitFailure, e:
822 DieWithError(
823 ('%s\nMaybe your depot_tools is out of date?\n'
824 'If all fails, contact maruel@') % e)
825
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000826 def UpdateDescription(self, description):
827 self.description = description
828 return self.RpcServer().update_description(
829 self.GetIssue(), self.description)
830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000832 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000833 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000835 def SetFlag(self, flag, value):
836 """Patchset must match."""
837 if not self.GetPatchset():
838 DieWithError('The patchset needs to match. Send another patchset.')
839 try:
840 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000841 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000842 except urllib2.HTTPError, e:
843 if e.code == 404:
844 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
845 if e.code == 403:
846 DieWithError(
847 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
848 'match?') % (self.GetIssue(), self.GetPatchset()))
849 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000851 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852 """Returns an upload.RpcServer() to access this review's rietveld instance.
853 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000854 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000855 self._rpc_server = rietveld.CachingRietveld(
856 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000857 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858
859 def _IssueSetting(self):
860 """Return the git setting that stores this change's issue."""
861 return 'branch.%s.rietveldissue' % self.GetBranch()
862
863 def _PatchsetSetting(self):
864 """Return the git setting that stores this change's most recent patchset."""
865 return 'branch.%s.rietveldpatchset' % self.GetBranch()
866
867 def _RietveldServer(self):
868 """Returns the git setting that stores this change's rietveld server."""
869 return 'branch.%s.rietveldserver' % self.GetBranch()
870
871
872def GetCodereviewSettingsInteractively():
873 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000874 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 server = settings.GetDefaultServerUrl(error_ok=True)
876 prompt = 'Rietveld server (host[:port])'
877 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000878 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 if not server and not newserver:
880 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000881 if newserver:
882 newserver = gclient_utils.UpgradeToHttps(newserver)
883 if newserver != server:
884 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000886 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887 prompt = caption
888 if initial:
889 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000890 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891 if new_val == 'x':
892 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000893 elif new_val:
894 if is_url:
895 new_val = gclient_utils.UpgradeToHttps(new_val)
896 if new_val != initial:
897 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000899 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000900 SetProperty(settings.GetDefaultPrivateFlag(),
901 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000903 'tree-status-url', False)
904 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000905 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000906
907 # TODO: configure a default branch to diff against, rather than this
908 # svn-based hackery.
909
910
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000911class ChangeDescription(object):
912 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000913 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000914 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000915
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000916 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000917 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000918
agable@chromium.org42c20792013-09-12 17:34:49 +0000919 @property # www.logilab.org/ticket/89786
920 def description(self): # pylint: disable=E0202
921 return '\n'.join(self._description_lines)
922
923 def set_description(self, desc):
924 if isinstance(desc, basestring):
925 lines = desc.splitlines()
926 else:
927 lines = [line.rstrip() for line in desc]
928 while lines and not lines[0]:
929 lines.pop(0)
930 while lines and not lines[-1]:
931 lines.pop(-1)
932 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000933
934 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000935 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000936 assert isinstance(reviewers, list), reviewers
937 if not reviewers:
938 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000939 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000940
agable@chromium.org42c20792013-09-12 17:34:49 +0000941 # Get the set of R= and TBR= lines and remove them from the desciption.
942 regexp = re.compile(self.R_LINE)
943 matches = [regexp.match(line) for line in self._description_lines]
944 new_desc = [l for i, l in enumerate(self._description_lines)
945 if not matches[i]]
946 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000947
agable@chromium.org42c20792013-09-12 17:34:49 +0000948 # Construct new unified R= and TBR= lines.
949 r_names = []
950 tbr_names = []
951 for match in matches:
952 if not match:
953 continue
954 people = cleanup_list([match.group(2).strip()])
955 if match.group(1) == 'TBR':
956 tbr_names.extend(people)
957 else:
958 r_names.extend(people)
959 for name in r_names:
960 if name not in reviewers:
961 reviewers.append(name)
962 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
963 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
964
965 # Put the new lines in the description where the old first R= line was.
966 line_loc = next((i for i, match in enumerate(matches) if match), -1)
967 if 0 <= line_loc < len(self._description_lines):
968 if new_tbr_line:
969 self._description_lines.insert(line_loc, new_tbr_line)
970 if new_r_line:
971 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000972 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000973 if new_r_line:
974 self.append_footer(new_r_line)
975 if new_tbr_line:
976 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000977
978 def prompt(self):
979 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000980 self.set_description([
981 '# Enter a description of the change.',
982 '# This will be displayed on the codereview site.',
983 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000984 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000985 '--------------------',
986 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000987
agable@chromium.org42c20792013-09-12 17:34:49 +0000988 regexp = re.compile(self.BUG_LINE)
989 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000990 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000991 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000992 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000993 if not content:
994 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000995 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000996
997 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000998 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
999 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001000 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001001 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001002
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001003 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001004 if self._description_lines:
1005 # Add an empty line if either the last line or the new line isn't a tag.
1006 last_line = self._description_lines[-1]
1007 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1008 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1009 self._description_lines.append('')
1010 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001011
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001012 def get_reviewers(self):
1013 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001014 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1015 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001016 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001017
1018
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001019def get_approving_reviewers(props):
1020 """Retrieves the reviewers that approved a CL from the issue properties with
1021 messages.
1022
1023 Note that the list may contain reviewers that are not committer, thus are not
1024 considered by the CQ.
1025 """
1026 return sorted(
1027 set(
1028 message['sender']
1029 for message in props['messages']
1030 if message['approval'] and message['sender'] in props['reviewers']
1031 )
1032 )
1033
1034
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001035def FindCodereviewSettingsFile(filename='codereview.settings'):
1036 """Finds the given file starting in the cwd and going up.
1037
1038 Only looks up to the top of the repository unless an
1039 'inherit-review-settings-ok' file exists in the root of the repository.
1040 """
1041 inherit_ok_file = 'inherit-review-settings-ok'
1042 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001043 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1045 root = '/'
1046 while True:
1047 if filename in os.listdir(cwd):
1048 if os.path.isfile(os.path.join(cwd, filename)):
1049 return open(os.path.join(cwd, filename))
1050 if cwd == root:
1051 break
1052 cwd = os.path.dirname(cwd)
1053
1054
1055def LoadCodereviewSettingsFromFile(fileobj):
1056 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001057 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 def SetProperty(name, setting, unset_error_ok=False):
1060 fullname = 'rietveld.' + name
1061 if setting in keyvals:
1062 RunGit(['config', fullname, keyvals[setting]])
1063 else:
1064 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1065
1066 SetProperty('server', 'CODE_REVIEW_SERVER')
1067 # Only server setting is required. Other settings can be absent.
1068 # In that case, we ignore errors raised during option deletion attempt.
1069 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001070 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1072 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001073 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001074 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1075 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001076 SetProperty('project', 'PROJECT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001078 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001079 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001080
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1082 #should be of the form
1083 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1084 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1085 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1086 keyvals['ORIGIN_URL_CONFIG']])
1087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001089def urlretrieve(source, destination):
1090 """urllib is broken for SSL connections via a proxy therefore we
1091 can't use urllib.urlretrieve()."""
1092 with open(destination, 'w') as f:
1093 f.write(urllib2.urlopen(source).read())
1094
1095
ukai@chromium.org712d6102013-11-27 00:52:58 +00001096def hasSheBang(fname):
1097 """Checks fname is a #! script."""
1098 with open(fname) as f:
1099 return f.read(2).startswith('#!')
1100
1101
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001102def DownloadHooks(force):
1103 """downloads hooks
1104
1105 Args:
1106 force: True to update hooks. False to install hooks if not present.
1107 """
1108 if not settings.GetIsGerrit():
1109 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001110 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001111 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1112 if not os.access(dst, os.X_OK):
1113 if os.path.exists(dst):
1114 if not force:
1115 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001116 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001117 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001118 if not hasSheBang(dst):
1119 DieWithError('Not a script: %s\n'
1120 'You need to download from\n%s\n'
1121 'into .git/hooks/commit-msg and '
1122 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001123 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1124 except Exception:
1125 if os.path.exists(dst):
1126 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001127 DieWithError('\nFailed to download hooks.\n'
1128 'You need to download from\n%s\n'
1129 'into .git/hooks/commit-msg and '
1130 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001131
1132
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001133@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001135 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001137 parser.add_option('--activate-update', action='store_true',
1138 help='activate auto-updating [rietveld] section in '
1139 '.git/config')
1140 parser.add_option('--deactivate-update', action='store_true',
1141 help='deactivate auto-updating [rietveld] section in '
1142 '.git/config')
1143 options, args = parser.parse_args(args)
1144
1145 if options.deactivate_update:
1146 RunGit(['config', 'rietveld.autoupdate', 'false'])
1147 return
1148
1149 if options.activate_update:
1150 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1151 return
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 if len(args) == 0:
1154 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001155 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 return 0
1157
1158 url = args[0]
1159 if not url.endswith('codereview.settings'):
1160 url = os.path.join(url, 'codereview.settings')
1161
1162 # Load code review settings and download hooks (if available).
1163 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001164 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 return 0
1166
1167
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001168def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001169 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001170 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1171 branch = ShortBranchName(branchref)
1172 _, args = parser.parse_args(args)
1173 if not args:
1174 print("Current base-url:")
1175 return RunGit(['config', 'branch.%s.base-url' % branch],
1176 error_ok=False).strip()
1177 else:
1178 print("Setting base-url to %s" % args[0])
1179 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1180 error_ok=False).strip()
1181
1182
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001184 """Show status of changelists.
1185
1186 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001187 - Red not sent for review or broken
1188 - Blue waiting for review
1189 - Yellow waiting for you to reply to review
1190 - Green LGTM'ed
1191 - Magenta in the commit queue
1192 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001193
1194 Also see 'git cl comments'.
1195 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 parser.add_option('--field',
1197 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001198 parser.add_option('-f', '--fast', action='store_true',
1199 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001201 if args:
1202 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001205 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 if options.field.startswith('desc'):
1207 print cl.GetDescription()
1208 elif options.field == 'id':
1209 issueid = cl.GetIssue()
1210 if issueid:
1211 print issueid
1212 elif options.field == 'patch':
1213 patchset = cl.GetPatchset()
1214 if patchset:
1215 print patchset
1216 elif options.field == 'url':
1217 url = cl.GetIssueURL()
1218 if url:
1219 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001220 return 0
1221
1222 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1223 if not branches:
1224 print('No local branch found.')
1225 return 0
1226
1227 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001228 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001229 alignment = max(5, max(len(b) for b in branches))
1230 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001231 # Adhoc thread pool to request data concurrently.
1232 output = Queue.Queue()
1233
1234 # Silence upload.py otherwise it becomes unweldly.
1235 upload.verbosity = 0
1236
1237 if not options.fast:
1238 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001239 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001240 c = Changelist(branchref=b)
1241 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001242 props = {}
1243 r = None
1244 if i:
1245 try:
1246 props = c.GetIssueProperties()
1247 r = c.GetApprovingReviewers() if i else None
1248 except urllib2.HTTPError:
1249 # The issue probably doesn't exist anymore.
1250 i += ' (broken)'
1251
1252 msgs = props.get('messages') or []
1253
1254 if not i:
1255 color = Fore.WHITE
1256 elif props.get('closed'):
1257 # Issue is closed.
1258 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001259 elif props.get('commit'):
1260 # Issue is in the commit queue.
1261 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001262 elif r:
1263 # Was LGTM'ed.
1264 color = Fore.GREEN
1265 elif not msgs:
1266 # No message was sent.
1267 color = Fore.RED
1268 elif msgs[-1]['sender'] != props.get('owner_email'):
1269 color = Fore.YELLOW
1270 else:
1271 color = Fore.BLUE
1272 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001273
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001274 # Process one branch synchronously to work through authentication, then
1275 # spawn threads to process all the other branches in parallel.
1276 if branches:
1277 fetch(branches[0])
1278 threads = [
1279 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001280 for t in threads:
1281 t.daemon = True
1282 t.start()
1283 else:
1284 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1285 for b in branches:
1286 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001287 url = c.GetIssueURL()
1288 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001289
1290 tmp = {}
1291 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001292 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001293 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001294 b, i, color = output.get()
1295 tmp[b] = (i, color)
1296 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001297 reset = Fore.RESET
1298 if not sys.stdout.isatty():
1299 color = ''
1300 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001301 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001302 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001303
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001304 cl = Changelist()
1305 print
1306 print 'Current branch:',
1307 if not cl.GetIssue():
1308 print 'no issue assigned.'
1309 return 0
1310 print cl.GetBranch()
1311 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1312 print 'Issue description:'
1313 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 return 0
1315
1316
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001317def colorize_CMDstatus_doc():
1318 """To be called once in main() to add colors to git cl status help."""
1319 colors = [i for i in dir(Fore) if i[0].isupper()]
1320
1321 def colorize_line(line):
1322 for color in colors:
1323 if color in line.upper():
1324 # Extract whitespaces first and the leading '-'.
1325 indent = len(line) - len(line.lstrip(' ')) + 1
1326 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1327 return line
1328
1329 lines = CMDstatus.__doc__.splitlines()
1330 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1331
1332
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001333@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001335 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336
1337 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001338 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001339 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340
1341 cl = Changelist()
1342 if len(args) > 0:
1343 try:
1344 issue = int(args[0])
1345 except ValueError:
1346 DieWithError('Pass a number to set the issue or none to list it.\n'
1347 'Maybe you want to run git cl status?')
1348 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001349 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350 return 0
1351
1352
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001353def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001354 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001355 (_, args) = parser.parse_args(args)
1356 if args:
1357 parser.error('Unsupported argument: %s' % args)
1358
1359 cl = Changelist()
1360 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001361 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001362 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001363 if message['disapproval']:
1364 color = Fore.RED
1365 elif message['approval']:
1366 color = Fore.GREEN
1367 elif message['sender'] == data['owner_email']:
1368 color = Fore.MAGENTA
1369 else:
1370 color = Fore.BLUE
1371 print '\n%s%s %s%s' % (
1372 color, message['date'].split('.', 1)[0], message['sender'],
1373 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001374 if message['text'].strip():
1375 print '\n'.join(' ' + l for l in message['text'].splitlines())
1376 return 0
1377
1378
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001379def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001380 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001381 cl = Changelist()
1382 if not cl.GetIssue():
1383 DieWithError('This branch has no associated changelist.')
1384 description = ChangeDescription(cl.GetDescription())
1385 description.prompt()
1386 cl.UpdateDescription(description.description)
1387 return 0
1388
1389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390def CreateDescriptionFromLog(args):
1391 """Pulls out the commit log to use as a base for the CL description."""
1392 log_args = []
1393 if len(args) == 1 and not args[0].endswith('.'):
1394 log_args = [args[0] + '..']
1395 elif len(args) == 1 and args[0].endswith('...'):
1396 log_args = [args[0][:-1]]
1397 elif len(args) == 2:
1398 log_args = [args[0] + '..' + args[1]]
1399 else:
1400 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001401 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402
1403
thestig@chromium.org44202a22014-03-11 19:22:18 +00001404def CMDlint(parser, args):
1405 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001406 parser.add_option('--filter', action='append', metavar='-x,+y',
1407 help='Comma-separated list of cpplint\'s category-filters')
1408 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001409
1410 # Access to a protected member _XX of a client class
1411 # pylint: disable=W0212
1412 try:
1413 import cpplint
1414 import cpplint_chromium
1415 except ImportError:
1416 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1417 return 1
1418
1419 # Change the current working directory before calling lint so that it
1420 # shows the correct base.
1421 previous_cwd = os.getcwd()
1422 os.chdir(settings.GetRoot())
1423 try:
1424 cl = Changelist()
1425 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1426 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001427 if not files:
1428 print "Cannot lint an empty CL"
1429 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001430
1431 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001432 command = args + files
1433 if options.filter:
1434 command = ['--filter=' + ','.join(options.filter)] + command
1435 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001436
1437 white_regex = re.compile(settings.GetLintRegex())
1438 black_regex = re.compile(settings.GetLintIgnoreRegex())
1439 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1440 for filename in filenames:
1441 if white_regex.match(filename):
1442 if black_regex.match(filename):
1443 print "Ignoring file %s" % filename
1444 else:
1445 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1446 extra_check_functions)
1447 else:
1448 print "Skipping file %s" % filename
1449 finally:
1450 os.chdir(previous_cwd)
1451 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1452 if cpplint._cpplint_state.error_count != 0:
1453 return 1
1454 return 0
1455
1456
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001458 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001459 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001461 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001462 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 (options, args) = parser.parse_args(args)
1464
ukai@chromium.org259e4682012-10-25 07:36:33 +00001465 if not options.force and is_dirty_git_tree('presubmit'):
1466 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 return 1
1468
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001469 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 if args:
1471 base_branch = args[0]
1472 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001473 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001474 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001476 cl.RunHook(
1477 committing=not options.upload,
1478 may_prompt=False,
1479 verbose=options.verbose,
1480 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001481 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482
1483
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001484def AddChangeIdToCommitMessage(options, args):
1485 """Re-commits using the current message, assumes the commit hook is in
1486 place.
1487 """
1488 log_desc = options.message or CreateDescriptionFromLog(args)
1489 git_command = ['commit', '--amend', '-m', log_desc]
1490 RunGit(git_command)
1491 new_log_desc = CreateDescriptionFromLog(args)
1492 if CHANGE_ID in new_log_desc:
1493 print 'git-cl: Added Change-Id to commit message.'
1494 else:
1495 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1496
1497
ukai@chromium.orge8077812012-02-03 03:41:46 +00001498def GerritUpload(options, args, cl):
1499 """upload the current branch to gerrit."""
1500 # We assume the remote called "origin" is the one we want.
1501 # It is probably not worthwhile to support different workflows.
1502 remote = 'origin'
1503 branch = 'master'
1504 if options.target_branch:
1505 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001507 change_desc = ChangeDescription(
1508 options.message or CreateDescriptionFromLog(args))
1509 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001510 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001511 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001512 if CHANGE_ID not in change_desc.description:
1513 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001514
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001515 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001516 if len(commits) > 1:
1517 print('WARNING: This will upload %d commits. Run the following command '
1518 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001519 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001520 print('You can also use `git squash-branch` to squash these into a single'
1521 'commit.')
1522 ask_for_data('About to upload; enter to confirm.')
1523
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001524 if options.reviewers:
1525 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526
ukai@chromium.orge8077812012-02-03 03:41:46 +00001527 receive_options = []
1528 cc = cl.GetCCList().split(',')
1529 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001530 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001531 cc = filter(None, cc)
1532 if cc:
1533 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001534 if change_desc.get_reviewers():
1535 receive_options.extend(
1536 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537
ukai@chromium.orge8077812012-02-03 03:41:46 +00001538 git_command = ['push']
1539 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001540 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541 ' '.join(receive_options))
1542 git_command += [remote, 'HEAD:refs/for/' + branch]
1543 RunGit(git_command)
1544 # TODO(ukai): parse Change-Id: and set issue number?
1545 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001546
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547
ukai@chromium.orge8077812012-02-03 03:41:46 +00001548def RietveldUpload(options, args, cl):
1549 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1551 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 if options.emulate_svn_auto_props:
1553 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554
1555 change_desc = None
1556
pgervais@chromium.org91141372014-01-09 23:27:20 +00001557 if options.email is not None:
1558 upload_args.extend(['--email', options.email])
1559
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001561 if options.title:
1562 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001563 if options.message:
1564 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001565 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566 print ("This branch is associated with issue %s. "
1567 "Adding patch to that issue." % cl.GetIssue())
1568 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001569 if options.title:
1570 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001571 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001572 change_desc = ChangeDescription(message)
1573 if options.reviewers:
1574 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001575 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001576 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001577
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001578 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579 print "Description is empty; aborting."
1580 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001581
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001582 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001583 if change_desc.get_reviewers():
1584 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001585 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001586 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001587 DieWithError("Must specify reviewers to send email.")
1588 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001589
1590 # We check this before applying rietveld.private assuming that in
1591 # rietveld.cc only addresses which we can send private CLs to are listed
1592 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1593 # --private is specified explicitly on the command line.
1594 if options.private:
1595 logging.warn('rietveld.cc is ignored since private flag is specified. '
1596 'You need to review and add them manually if necessary.')
1597 cc = cl.GetCCListWithoutDefault()
1598 else:
1599 cc = cl.GetCCList()
1600 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001601 if cc:
1602 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001603
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001604 if options.private or settings.GetDefaultPrivateFlag() == "True":
1605 upload_args.append('--private')
1606
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001607 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001608 if not options.find_copies:
1609 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001610
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611 # Include the upstream repo's URL in the change -- this is useful for
1612 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001613 remote_url = cl.GetGitBaseUrlFromConfig()
1614 if not remote_url:
1615 if settings.GetIsGitSvn():
1616 # URL is dependent on the current directory.
1617 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1618 if data:
1619 keys = dict(line.split(': ', 1) for line in data.splitlines()
1620 if ': ' in line)
1621 remote_url = keys.get('URL', None)
1622 else:
1623 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1624 remote_url = (cl.GetRemoteUrl() + '@'
1625 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626 if remote_url:
1627 upload_args.extend(['--base_url', remote_url])
1628
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001629 project = settings.GetProject()
1630 if project:
1631 upload_args.extend(['--project', project])
1632
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001633 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001634 upload_args = ['upload'] + upload_args + args
1635 logging.info('upload.RealMain(%s)', upload_args)
1636 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001637 issue = int(issue)
1638 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001639 except KeyboardInterrupt:
1640 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641 except:
1642 # If we got an exception after the user typed a description for their
1643 # change, back up the description before re-raising.
1644 if change_desc:
1645 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1646 print '\nGot exception while uploading -- saving description to %s\n' \
1647 % backup_path
1648 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001649 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650 backup_file.close()
1651 raise
1652
1653 if not cl.GetIssue():
1654 cl.SetIssue(issue)
1655 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001656
1657 if options.use_commit_queue:
1658 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001659 return 0
1660
1661
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001662def cleanup_list(l):
1663 """Fixes a list so that comma separated items are put as individual items.
1664
1665 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1666 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1667 """
1668 items = sum((i.split(',') for i in l), [])
1669 stripped_items = (i.strip() for i in items)
1670 return sorted(filter(None, stripped_items))
1671
1672
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001673@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001674def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001675 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001676 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1677 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001678 parser.add_option('--bypass-watchlists', action='store_true',
1679 dest='bypass_watchlists',
1680 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001681 parser.add_option('-f', action='store_true', dest='force',
1682 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001683 parser.add_option('-m', dest='message', help='message for patchset')
1684 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001685 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001686 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001687 help='reviewer email addresses')
1688 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001689 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001690 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001691 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001692 help='send email to reviewer immediately')
1693 parser.add_option("--emulate_svn_auto_props", action="store_true",
1694 dest="emulate_svn_auto_props",
1695 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001696 parser.add_option('-c', '--use-commit-queue', action='store_true',
1697 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001698 parser.add_option('--private', action='store_true',
1699 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001700 parser.add_option('--target_branch',
1701 help='When uploading to gerrit, remote branch to '
1702 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001703 parser.add_option('--email', default=None,
1704 help='email address to use to connect to Rietveld')
1705
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001706 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001707 (options, args) = parser.parse_args(args)
1708
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001709 if options.target_branch and not settings.GetIsGerrit():
1710 parser.error('Use --target_branch for non gerrit repository.')
1711
ukai@chromium.org259e4682012-10-25 07:36:33 +00001712 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001713 return 1
1714
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001715 options.reviewers = cleanup_list(options.reviewers)
1716 options.cc = cleanup_list(options.cc)
1717
ukai@chromium.orge8077812012-02-03 03:41:46 +00001718 cl = Changelist()
1719 if args:
1720 # TODO(ukai): is it ok for gerrit case?
1721 base_branch = args[0]
1722 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001723 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001724 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001725 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001726
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001727 # Apply watchlists on upload.
1728 change = cl.GetChange(base_branch, None)
1729 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1730 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001731 if not options.bypass_watchlists:
1732 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001733
ukai@chromium.orge8077812012-02-03 03:41:46 +00001734 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001735 if options.reviewers:
1736 # Set the reviewer list now so that presubmit checks can access it.
1737 change_description = ChangeDescription(change.FullDescriptionText())
1738 change_description.update_reviewers(options.reviewers)
1739 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001740 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001741 may_prompt=not options.force,
1742 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001743 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001744 if not hook_results.should_continue():
1745 return 1
1746 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001747 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001748
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001749 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001750 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001751 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001752 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001753 print ('The last upload made from this repository was patchset #%d but '
1754 'the most recent patchset on the server is #%d.'
1755 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001756 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1757 'from another machine or branch the patch you\'re uploading now '
1758 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001759 ask_for_data('About to upload; enter to confirm.')
1760
iannucci@chromium.org79540052012-10-19 23:15:26 +00001761 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001762 if settings.GetIsGerrit():
1763 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001764 ret = RietveldUpload(options, args, cl)
1765 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001766 git_set_branch_value('last-upload-hash',
1767 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001768
1769 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001770
1771
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001772def IsSubmoduleMergeCommit(ref):
1773 # When submodules are added to the repo, we expect there to be a single
1774 # non-git-svn merge commit at remote HEAD with a signature comment.
1775 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001776 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001777 return RunGit(cmd) != ''
1778
1779
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001781 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001783 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784 Updates changelog with metadata (e.g. pointer to review).
1785 Pushes/dcommits the code upstream.
1786 Updates review and closes.
1787 """
1788 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1789 help='bypass upload presubmit hook')
1790 parser.add_option('-m', dest='message',
1791 help="override review description")
1792 parser.add_option('-f', action='store_true', dest='force',
1793 help="force yes to questions (don't prompt)")
1794 parser.add_option('-c', dest='contributor',
1795 help="external contributor for patch (appended to " +
1796 "description and used as author for git). Should be " +
1797 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001798 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001799 (options, args) = parser.parse_args(args)
1800 cl = Changelist()
1801
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001802 current = cl.GetBranch()
1803 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1804 if not settings.GetIsGitSvn() and remote == '.':
1805 print
1806 print 'Attempting to push branch %r into another local branch!' % current
1807 print
1808 print 'Either reparent this branch on top of origin/master:'
1809 print ' git reparent-branch --root'
1810 print
1811 print 'OR run `git rebase-update` if you think the parent branch is already'
1812 print 'committed.'
1813 print
1814 print ' Current parent: %r' % upstream_branch
1815 return 1
1816
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817 if not args or cmd == 'push':
1818 # Default to merging against our best guess of the upstream branch.
1819 args = [cl.GetUpstreamBranch()]
1820
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001821 if options.contributor:
1822 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1823 print "Please provide contibutor as 'First Last <email@example.com>'"
1824 return 1
1825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001826 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001827 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828
ukai@chromium.org259e4682012-10-25 07:36:33 +00001829 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830 return 1
1831
1832 # This rev-list syntax means "show all commits not in my branch that
1833 # are in base_branch".
1834 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1835 base_branch]).splitlines()
1836 if upstream_commits:
1837 print ('Base branch "%s" has %d commits '
1838 'not in this branch.' % (base_branch, len(upstream_commits)))
1839 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1840 return 1
1841
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001842 # This is the revision `svn dcommit` will commit on top of.
1843 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1844 '--pretty=format:%H'])
1845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001847 # If the base_head is a submodule merge commit, the first parent of the
1848 # base_head should be a git-svn commit, which is what we're interested in.
1849 base_svn_head = base_branch
1850 if base_has_submodules:
1851 base_svn_head += '^1'
1852
1853 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854 if extra_commits:
1855 print ('This branch has %d additional commits not upstreamed yet.'
1856 % len(extra_commits.splitlines()))
1857 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1858 'before attempting to %s.' % (base_branch, cmd))
1859 return 1
1860
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001861 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001862 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001863 author = None
1864 if options.contributor:
1865 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001866 hook_results = cl.RunHook(
1867 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001868 may_prompt=not options.force,
1869 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001870 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001871 if not hook_results.should_continue():
1872 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001873
1874 if cmd == 'dcommit':
1875 # Check the tree status if the tree status URL is set.
1876 status = GetTreeStatus()
1877 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001878 print('The tree is closed. Please wait for it to reopen. Use '
1879 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880 return 1
1881 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001882 print('Unable to determine tree status. Please verify manually and '
1883 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001884 else:
1885 breakpad.SendStack(
1886 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001887 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1888 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001889 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001890
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001891 change_desc = ChangeDescription(options.message)
1892 if not change_desc.description and cl.GetIssue():
1893 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001895 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001896 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001897 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001898 else:
1899 print 'No description set.'
1900 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1901 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001903 # Keep a separate copy for the commit message, because the commit message
1904 # contains the link to the Rietveld issue, while the Rietveld message contains
1905 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001906 # Keep a separate copy for the commit message.
1907 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001908 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001909
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001910 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001911 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001912 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001914 commit_desc.append_footer('Patch from %s.' % options.contributor)
1915
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001916 print('Description:')
1917 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918
1919 branches = [base_branch, cl.GetBranchRef()]
1920 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001921 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001922 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001924 # We want to squash all this branch's commits into one commit with the proper
1925 # description. We do this by doing a "reset --soft" to the base branch (which
1926 # keeps the working copy the same), then dcommitting that. If origin/master
1927 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1928 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001930 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1931 # Delete the branches if they exist.
1932 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1933 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1934 result = RunGitWithCode(showref_cmd)
1935 if result[0] == 0:
1936 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001937
1938 # We might be in a directory that's present in this branch but not in the
1939 # trunk. Move up to the top of the tree so that git commands that expect a
1940 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001941 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942 if rel_base_path:
1943 os.chdir(rel_base_path)
1944
1945 # Stuff our change into the merge branch.
1946 # We wrap in a try...finally block so if anything goes wrong,
1947 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001948 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001950 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1951 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001953 RunGit(
1954 [
1955 'commit', '--author', options.contributor,
1956 '-m', commit_desc.description,
1957 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001959 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001960 if base_has_submodules:
1961 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1962 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1963 RunGit(['checkout', CHERRY_PICK_BRANCH])
1964 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 if cmd == 'push':
1966 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001967 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968 retcode, output = RunGitWithCode(
1969 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1970 logging.debug(output)
1971 else:
1972 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001973 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001974 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001975 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 finally:
1977 # And then swap back to the original branch and clean up.
1978 RunGit(['checkout', '-q', cl.GetBranch()])
1979 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001980 if base_has_submodules:
1981 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001982
1983 if cl.GetIssue():
1984 if cmd == 'dcommit' and 'Committed r' in output:
1985 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1986 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001987 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1988 for l in output.splitlines(False))
1989 match = filter(None, match)
1990 if len(match) != 1:
1991 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1992 output)
1993 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994 else:
1995 return 1
1996 viewvc_url = settings.GetViewVCUrl()
1997 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001998 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001999 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002000 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001 print ('Closing issue '
2002 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002003 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002005 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002006 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00002007 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002008 if options.bypass_hooks:
2009 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2010 else:
2011 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002012 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002013 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002014
2015 if retcode == 0:
2016 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2017 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002018 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002019
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020 return 0
2021
2022
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002023@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002025 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002027 message = """This doesn't appear to be an SVN repository.
2028If your project has a git mirror with an upstream SVN master, you probably need
2029to run 'git svn init', see your project's git mirror documentation.
2030If your project has a true writeable upstream repository, you probably want
2031to run 'git cl push' instead.
2032Choose wisely, if you get this wrong, your commit might appear to succeed but
2033will instead be silently ignored."""
2034 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002035 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036 return SendUpstream(parser, args, 'dcommit')
2037
2038
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002039@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002040def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002041 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042 if settings.GetIsGitSvn():
2043 print('This appears to be an SVN repository.')
2044 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002045 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046 return SendUpstream(parser, args, 'push')
2047
2048
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002049@subcommand.usage('[upstream branch to apply against]')
2050def CMDpush(parser, args):
2051 """Temporary alias for 'land'.
2052 """
2053 print(
2054 "\n=======\n"
2055 "'git cl push' has been renamed to 'git cl land'.\n"
2056 "Currently they are treated as synonyms, but 'git cl push' will stop\n"
2057 "working after 2014/07/01.\n"
2058 "=======\n")
2059 now = datetime.datetime.utcfromtimestamp(time.time())
2060 if now > datetime.datetime(2014, 7, 1):
2061 print('******\nReally, you should not use this command anymore... \n'
2062 'Pausing 10 sec to help you remember :-)')
2063 for n in xrange(10):
2064 time.sleep(1)
2065 print('%s seconds...' % (n+1))
2066 print('******')
2067 return CMDland(parser, args)
2068
2069
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002070@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002072 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002073 parser.add_option('-b', dest='newbranch',
2074 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002075 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002077 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2078 help='Change to the directory DIR immediately, '
2079 'before doing anything else.')
2080 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002081 help='failed patches spew .rej files rather than '
2082 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2084 help="don't commit after patch applies")
2085 (options, args) = parser.parse_args(args)
2086 if len(args) != 1:
2087 parser.print_help()
2088 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002089 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002091 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002092 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002093
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002094 if options.newbranch:
2095 if options.force:
2096 RunGit(['branch', '-D', options.newbranch],
2097 stderr=subprocess2.PIPE, error_ok=True)
2098 RunGit(['checkout', '-b', options.newbranch,
2099 Changelist().GetUpstreamBranch()])
2100
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002101 return PatchIssue(issue_arg, options.reject, options.nocommit,
2102 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002103
2104
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002105def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002106 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002108 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002109 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002110 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002111 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002112 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002113 # Assume it's a URL to the patch. Default to https.
2114 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002115 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002116 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 DieWithError('Must pass an issue ID or full URL for '
2118 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002119 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002120 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002121 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 # Switch up to the top-level directory, if necessary, in preparation for
2124 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002125 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126 if top:
2127 os.chdir(top)
2128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129 # Git patches have a/ at the beginning of source paths. We strip that out
2130 # with a sed script rather than the -p flag to patch so we can feed either
2131 # Git or svn-style patches into the same apply command.
2132 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002133 try:
2134 patch_data = subprocess2.check_output(
2135 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2136 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002137 DieWithError('Git patch mungling failed.')
2138 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002140 # We use "git apply" to apply the patch instead of "patch" so that we can
2141 # pick up file adds.
2142 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002143 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002144 if directory:
2145 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002146 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002148 elif IsGitVersionAtLeast('1.7.12'):
2149 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002150 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002151 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002152 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002153 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154 DieWithError('Failed to apply the patch')
2155
2156 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002157 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2159 cl = Changelist()
2160 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002161 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002162 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002163 else:
2164 print "Patch applied to index."
2165 return 0
2166
2167
2168def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002169 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002170 # Provide a wrapper for git svn rebase to help avoid accidental
2171 # git svn dcommit.
2172 # It's the only command that doesn't use parser at all since we just defer
2173 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002174
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002175 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002176
2177
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002178def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002179 """Fetches the tree status and returns either 'open', 'closed',
2180 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002181 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002182 if url:
2183 status = urllib2.urlopen(url).read().lower()
2184 if status.find('closed') != -1 or status == '0':
2185 return 'closed'
2186 elif status.find('open') != -1 or status == '1':
2187 return 'open'
2188 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002189 return 'unset'
2190
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192def GetTreeStatusReason():
2193 """Fetches the tree status from a json url and returns the message
2194 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002195 url = settings.GetTreeStatusUrl()
2196 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197 connection = urllib2.urlopen(json_url)
2198 status = json.loads(connection.read())
2199 connection.close()
2200 return status['message']
2201
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002202
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002204 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002205 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206 status = GetTreeStatus()
2207 if 'unset' == status:
2208 print 'You must configure your tree status URL by running "git cl config".'
2209 return 2
2210
2211 print "The tree is %s" % status
2212 print
2213 print GetTreeStatusReason()
2214 if status != 'open':
2215 return 1
2216 return 0
2217
2218
maruel@chromium.org15192402012-09-06 12:38:29 +00002219def CMDtry(parser, args):
2220 """Triggers a try job through Rietveld."""
2221 group = optparse.OptionGroup(parser, "Try job options")
2222 group.add_option(
2223 "-b", "--bot", action="append",
2224 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2225 "times to specify multiple builders. ex: "
2226 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2227 "the try server waterfall for the builders name and the tests "
2228 "available. Can also be used to specify gtest_filter, e.g. "
2229 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2230 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002231 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002232 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002233 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002234 "-r", "--revision",
2235 help="Revision to use for the try job; default: the "
2236 "revision will be determined by the try server; see "
2237 "its waterfall for more info")
2238 group.add_option(
2239 "-c", "--clobber", action="store_true", default=False,
2240 help="Force a clobber before building; e.g. don't do an "
2241 "incremental build")
2242 group.add_option(
2243 "--project",
2244 help="Override which project to use. Projects are defined "
2245 "server-side to define what default bot set to use")
2246 group.add_option(
2247 "-t", "--testfilter", action="append", default=[],
2248 help=("Apply a testfilter to all the selected builders. Unless the "
2249 "builders configurations are similar, use multiple "
2250 "--bot <builder>:<test> arguments."))
2251 group.add_option(
2252 "-n", "--name", help="Try job name; default to current branch name")
2253 parser.add_option_group(group)
2254 options, args = parser.parse_args(args)
2255
2256 if args:
2257 parser.error('Unknown arguments: %s' % args)
2258
2259 cl = Changelist()
2260 if not cl.GetIssue():
2261 parser.error('Need to upload first')
2262
2263 if not options.name:
2264 options.name = cl.GetBranch()
2265
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002266 if options.bot and not options.master:
2267 parser.error('For manually specified bots please also specify the '
2268 'tryserver master, e.g. "-m tryserver.chromium".')
2269
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002270 def GetMasterMap():
2271 # Process --bot and --testfilter.
2272 if not options.bot:
2273 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002274
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002275 # Get try masters from PRESUBMIT.py files.
2276 masters = presubmit_support.DoGetTryMasters(
2277 change,
2278 change.LocalPaths(),
2279 settings.GetRoot(),
2280 None,
2281 None,
2282 options.verbose,
2283 sys.stdout)
2284 if masters:
2285 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002286
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002287 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2288 options.bot = presubmit_support.DoGetTrySlaves(
2289 change,
2290 change.LocalPaths(),
2291 settings.GetRoot(),
2292 None,
2293 None,
2294 options.verbose,
2295 sys.stdout)
2296 if not options.bot:
2297 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002298
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002299 builders_and_tests = {}
2300 # TODO(machenbach): The old style command-line options don't support
2301 # multiple try masters yet.
2302 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2303 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2304
2305 for bot in old_style:
2306 if ':' in bot:
2307 builder, tests = bot.split(':', 1)
2308 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2309 elif ',' in bot:
2310 parser.error('Specify one bot per --bot flag')
2311 else:
2312 builders_and_tests.setdefault(bot, []).append('defaulttests')
2313
2314 for bot, tests in new_style:
2315 builders_and_tests.setdefault(bot, []).extend(tests)
2316
2317 # Return a master map with one master to be backwards compatible. The
2318 # master name defaults to an empty string, which will cause the master
2319 # not to be set on rietveld (deprecated).
2320 return {options.master: builders_and_tests}
2321
2322 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002323
maruel@chromium.org15192402012-09-06 12:38:29 +00002324 if options.testfilter:
2325 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002326 masters = dict((master, dict(
2327 (b, forced_tests) for b, t in slaves.iteritems()
2328 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002329
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002330 for builders in masters.itervalues():
2331 if any('triggered' in b for b in builders):
2332 print >> sys.stderr, (
2333 'ERROR You are trying to send a job to a triggered bot. This type of'
2334 ' bot requires an\ninitial job from a parent (usually a builder). '
2335 'Instead send your job to the parent.\n'
2336 'Bot list: %s' % builders)
2337 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002338
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002339 patchset = cl.GetMostRecentPatchset()
2340 if patchset and patchset != cl.GetPatchset():
2341 print(
2342 '\nWARNING Mismatch between local config and server. Did a previous '
2343 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2344 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002345 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002346 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002347 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002348 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002349 except urllib2.HTTPError, e:
2350 if e.code == 404:
2351 print('404 from rietveld; '
2352 'did you mean to use "git try" instead of "git cl try"?')
2353 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002354 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002355
2356 for (master, builders) in masters.iteritems():
2357 if master:
2358 print 'Master: %s' % master
2359 length = max(len(builder) for builder in builders)
2360 for builder in sorted(builders):
2361 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002362 return 0
2363
2364
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002365@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002366def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002367 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002368 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002369 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002370 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002371
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002372 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002373 if args:
2374 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002375 branch = cl.GetBranch()
2376 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002377 cl = Changelist()
2378 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002379
2380 # Clear configured merge-base, if there is one.
2381 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002382 else:
2383 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002384 return 0
2385
2386
thestig@chromium.org00858c82013-12-02 23:08:03 +00002387def CMDweb(parser, args):
2388 """Opens the current CL in the web browser."""
2389 _, args = parser.parse_args(args)
2390 if args:
2391 parser.error('Unrecognized args: %s' % ' '.join(args))
2392
2393 issue_url = Changelist().GetIssueURL()
2394 if not issue_url:
2395 print >> sys.stderr, 'ERROR No issue to open'
2396 return 1
2397
2398 webbrowser.open(issue_url)
2399 return 0
2400
2401
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002402def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002403 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002404 _, args = parser.parse_args(args)
2405 if args:
2406 parser.error('Unrecognized args: %s' % ' '.join(args))
2407 cl = Changelist()
2408 cl.SetFlag('commit', '1')
2409 return 0
2410
2411
groby@chromium.org411034a2013-02-26 15:12:01 +00002412def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002413 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002414 _, args = parser.parse_args(args)
2415 if args:
2416 parser.error('Unrecognized args: %s' % ' '.join(args))
2417 cl = Changelist()
2418 # Ensure there actually is an issue to close.
2419 cl.GetDescription()
2420 cl.CloseIssue()
2421 return 0
2422
2423
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002424def CMDdiff(parser, args):
2425 """shows differences between local tree and last upload."""
2426 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002427 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002428 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002429 if not issue:
2430 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002431 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002432 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002433
2434 # Create a new branch based on the merge-base
2435 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2436 try:
2437 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002438 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002439 if rtn != 0:
2440 return rtn
2441
2442 # Switch back to starting brand and diff against the temporary
2443 # branch containing the latest rietveld patch.
2444 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2445 finally:
2446 RunGit(['checkout', '-q', branch])
2447 RunGit(['branch', '-D', TMP_BRANCH])
2448
2449 return 0
2450
2451
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002452def CMDowners(parser, args):
2453 """interactively find the owners for reviewing"""
2454 parser.add_option(
2455 '--no-color',
2456 action='store_true',
2457 help='Use this option to disable color output')
2458 options, args = parser.parse_args(args)
2459
2460 author = RunGit(['config', 'user.email']).strip() or None
2461
2462 cl = Changelist()
2463
2464 if args:
2465 if len(args) > 1:
2466 parser.error('Unknown args')
2467 base_branch = args[0]
2468 else:
2469 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002470 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002471
2472 change = cl.GetChange(base_branch, None)
2473 return owners_finder.OwnersFinder(
2474 [f.LocalPath() for f in
2475 cl.GetChange(base_branch, None).AffectedFiles()],
2476 change.RepositoryRoot(), author,
2477 fopen=file, os_path=os.path, glob=glob.glob,
2478 disable_color=options.no_color).run()
2479
2480
enne@chromium.org555cfe42014-01-29 18:21:39 +00002481@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002482def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002483 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002484 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002485 parser.add_option('--full', action='store_true',
2486 help='Reformat the full content of all touched files')
2487 parser.add_option('--dry-run', action='store_true',
2488 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002489 parser.add_option('--diff', action='store_true',
2490 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002491 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002492
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002493 # git diff generates paths against the root of the repository. Change
2494 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002495 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002496 if rel_base_path:
2497 os.chdir(rel_base_path)
2498
digit@chromium.org29e47272013-05-17 17:01:46 +00002499 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002500 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002501 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002502 # Only list the names of modified files.
2503 diff_cmd.append('--name-only')
2504 else:
2505 # Only generate context-less patches.
2506 diff_cmd.append('-U0')
2507
2508 # Grab the merge-base commit, i.e. the upstream commit of the current
2509 # branch when it was created or the last time it was rebased. This is
2510 # to cover the case where the user may have called "git fetch origin",
2511 # moving the origin branch to a newer commit, but hasn't rebased yet.
2512 upstream_commit = None
2513 cl = Changelist()
2514 upstream_branch = cl.GetUpstreamBranch()
2515 if upstream_branch:
2516 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2517 upstream_commit = upstream_commit.strip()
2518
2519 if not upstream_commit:
2520 DieWithError('Could not find base commit for this branch. '
2521 'Are you in detached state?')
2522
2523 diff_cmd.append(upstream_commit)
2524
2525 # Handle source file filtering.
2526 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002527 if args:
2528 for arg in args:
2529 if os.path.isdir(arg):
2530 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2531 elif os.path.isfile(arg):
2532 diff_cmd.append(arg)
2533 else:
2534 DieWithError('Argument "%s" is not a file or a directory' % arg)
2535 else:
2536 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002537 diff_output = RunGit(diff_cmd)
2538
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002539 top_dir = os.path.normpath(
2540 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2541
2542 # Locate the clang-format binary in the checkout
2543 try:
2544 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2545 except clang_format.NotFoundError, e:
2546 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002547
digit@chromium.org29e47272013-05-17 17:01:46 +00002548 if opts.full:
2549 # diff_output is a list of files to send to clang-format.
2550 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002551 if not files:
2552 print "Nothing to format."
2553 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002554 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002555 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002556 cmd.append('-i')
2557 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002558 if opts.diff:
2559 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002560 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002561 env = os.environ.copy()
2562 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002563 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002564 try:
2565 script = clang_format.FindClangFormatScriptInChromiumTree(
2566 'clang-format-diff.py')
2567 except clang_format.NotFoundError, e:
2568 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002569
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002570 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002571 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002572 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002573
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002574 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002575 if opts.diff:
2576 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002577 if opts.dry_run and len(stdout) > 0:
2578 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002579
2580 return 0
2581
2582
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002583class OptionParser(optparse.OptionParser):
2584 """Creates the option parse and add --verbose support."""
2585 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002586 optparse.OptionParser.__init__(
2587 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002588 self.add_option(
2589 '-v', '--verbose', action='count', default=0,
2590 help='Use 2 times for more debugging info')
2591
2592 def parse_args(self, args=None, values=None):
2593 options, args = optparse.OptionParser.parse_args(self, args, values)
2594 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2595 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2596 return options, args
2597
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002599def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002600 if sys.hexversion < 0x02060000:
2601 print >> sys.stderr, (
2602 '\nYour python version %s is unsupported, please upgrade.\n' %
2603 sys.version.split(' ', 1)[0])
2604 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002605
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002606 # Reload settings.
2607 global settings
2608 settings = Settings()
2609
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002610 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002611 dispatcher = subcommand.CommandDispatcher(__name__)
2612 try:
2613 return dispatcher.execute(OptionParser(), argv)
2614 except urllib2.HTTPError, e:
2615 if e.code != 500:
2616 raise
2617 DieWithError(
2618 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2619 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002620
2621
2622if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002623 # These affect sys.stdout so do it outside of main() to simplify mocks in
2624 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002625 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002626 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002627 sys.exit(main(sys.argv[1:]))