blob: 59c2d4b1ff39e121629ad6c1788dba00605e28f0 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000038import git_common
39import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000041import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000043import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000044import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import watchlists
46
maruel@chromium.org0633fb42013-08-16 20:06:14 +000047__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000049DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000050POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000052GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000053CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054
thestig@chromium.org44202a22014-03-11 19:22:18 +000055# Valid extensions for files we want to lint.
56DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
57DEFAULT_LINT_IGNORE_REGEX = r"$^"
58
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000059# Shortcut since it quickly becomes redundant.
60Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000061
maruel@chromium.orgddd59412011-11-30 14:20:38 +000062# Initialized in main()
63settings = None
64
65
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000067 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 sys.exit(1)
69
70
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000071def GetNoGitPagerEnv():
72 env = os.environ.copy()
73 # 'cat' is a magical git string that disables pagers on all platforms.
74 env['GIT_PAGER'] = 'cat'
75 return env
76
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000079 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000080 except subprocess2.CalledProcessError as e:
81 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000082 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 'Command "%s" failed.\n%s' % (
85 ' '.join(args), error_message or e.stdout or ''))
86 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087
88
89def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000090 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000091 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000092
93
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000094def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000096 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000097 if suppress_stderr:
98 stderr = subprocess2.VOID
99 else:
100 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000101 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000102 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000103 stdout=subprocess2.PIPE,
104 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000105 return code, out[0]
106 except ValueError:
107 # When the subprocess fails, it returns None. That triggers a ValueError
108 # when trying to unpack the return value into (out, code).
109 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000112def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000113 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000114 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000115 return (version.startswith(prefix) and
116 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000117
118
maruel@chromium.org90541732011-04-01 17:54:18 +0000119def ask_for_data(prompt):
120 try:
121 return raw_input(prompt)
122 except KeyboardInterrupt:
123 # Hide the exception.
124 sys.exit(1)
125
126
iannucci@chromium.org79540052012-10-19 23:15:26 +0000127def git_set_branch_value(key, value):
128 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000129 if not branch:
130 return
131
132 cmd = ['config']
133 if isinstance(value, int):
134 cmd.append('--int')
135 git_key = 'branch.%s.%s' % (branch, key)
136 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000137
138
139def git_get_branch_default(key, default):
140 branch = Changelist().GetBranch()
141 if branch:
142 git_key = 'branch.%s.%s' % (branch, key)
143 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
144 try:
145 return int(stdout.strip())
146 except ValueError:
147 pass
148 return default
149
150
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000151def add_git_similarity(parser):
152 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000153 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000154 help='Sets the percentage that a pair of files need to match in order to'
155 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156 parser.add_option(
157 '--find-copies', action='store_true',
158 help='Allows git to look for copies.')
159 parser.add_option(
160 '--no-find-copies', action='store_false', dest='find_copies',
161 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000162
163 old_parser_args = parser.parse_args
164 def Parse(args):
165 options, args = old_parser_args(args)
166
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000167 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170 print('Note: Saving similarity of %d%% in git config.'
171 % options.similarity)
172 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000173
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 options.similarity = max(0, min(options.similarity, 100))
175
176 if options.find_copies is None:
177 options.find_copies = bool(
178 git_get_branch_default('git-find-copies', True))
179 else:
180 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000181
182 print('Using %d%% similarity for rename/copy detection. '
183 'Override with --similarity.' % options.similarity)
184
185 return options, args
186 parser.parse_args = Parse
187
188
ukai@chromium.org259e4682012-10-25 07:36:33 +0000189def is_dirty_git_tree(cmd):
190 # Make sure index is up-to-date before running diff-index.
191 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
192 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
193 if dirty:
194 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
195 print 'Uncommitted files: (git diff-index --name-status HEAD)'
196 print dirty[:4096]
197 if len(dirty) > 4096:
198 print '... (run "git diff-index --name-status HEAD" to see full output).'
199 return True
200 return False
201
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000202
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000203def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
204 """Return the corresponding git ref if |base_url| together with |glob_spec|
205 matches the full |url|.
206
207 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
208 """
209 fetch_suburl, as_ref = glob_spec.split(':')
210 if allow_wildcards:
211 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
212 if glob_match:
213 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
214 # "branches/{472,597,648}/src:refs/remotes/svn/*".
215 branch_re = re.escape(base_url)
216 if glob_match.group(1):
217 branch_re += '/' + re.escape(glob_match.group(1))
218 wildcard = glob_match.group(2)
219 if wildcard == '*':
220 branch_re += '([^/]*)'
221 else:
222 # Escape and replace surrounding braces with parentheses and commas
223 # with pipe symbols.
224 wildcard = re.escape(wildcard)
225 wildcard = re.sub('^\\\\{', '(', wildcard)
226 wildcard = re.sub('\\\\,', '|', wildcard)
227 wildcard = re.sub('\\\\}$', ')', wildcard)
228 branch_re += wildcard
229 if glob_match.group(3):
230 branch_re += re.escape(glob_match.group(3))
231 match = re.match(branch_re, url)
232 if match:
233 return re.sub('\*$', match.group(1), as_ref)
234
235 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
236 if fetch_suburl:
237 full_url = base_url + '/' + fetch_suburl
238 else:
239 full_url = base_url
240 if full_url == url:
241 return as_ref
242 return None
243
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000244
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000246 """Prints statistics about the change to the user."""
247 # --no-ext-diff is broken in some versions of Git, so try to work around
248 # this by overriding the environment (but there is still a problem if the
249 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000250 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000251 if 'GIT_EXTERNAL_DIFF' in env:
252 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000253
254 if find_copies:
255 similarity_options = ['--find-copies-harder', '-l100000',
256 '-C%s' % similarity]
257 else:
258 similarity_options = ['-M%s' % similarity]
259
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000260 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000261 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000262 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000263 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000264
265
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266class Settings(object):
267 def __init__(self):
268 self.default_server = None
269 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000270 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000271 self.is_git_svn = None
272 self.svn_branch = None
273 self.tree_status_url = None
274 self.viewvc_url = None
275 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000276 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000277 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278
279 def LazyUpdateIfNeeded(self):
280 """Updates the settings from a codereview.settings file, if available."""
281 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000282 # The only value that actually changes the behavior is
283 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000284 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000285 error_ok=True
286 ).strip().lower()
287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000289 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000291 # set updated to True to avoid infinite calling loop
292 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000293 self.updated = True
294 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 self.updated = True
296
297 def GetDefaultServerUrl(self, error_ok=False):
298 if not self.default_server:
299 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000300 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000301 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 if error_ok:
303 return self.default_server
304 if not self.default_server:
305 error_message = ('Could not find settings file. You must configure '
306 'your review setup by running "git cl config".')
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_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309 return self.default_server
310
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000311 @staticmethod
312 def GetRelativeRoot():
313 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000315 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000316 if self.root is None:
317 self.root = os.path.abspath(self.GetRelativeRoot())
318 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000319
320 def GetIsGitSvn(self):
321 """Return true if this repo looks like it's using git-svn."""
322 if self.is_git_svn is None:
323 # If you have any "svn-remote.*" config keys, we think you're using svn.
324 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000325 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326 return self.is_git_svn
327
328 def GetSVNBranch(self):
329 if self.svn_branch is None:
330 if not self.GetIsGitSvn():
331 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
332
333 # Try to figure out which remote branch we're based on.
334 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000335 # 1) iterate through our branch history and find the svn URL.
336 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337
338 # regexp matching the git-svn line that contains the URL.
339 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
340
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000341 # We don't want to go through all of history, so read a line from the
342 # pipe at a time.
343 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000344 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000345 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
346 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000347 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000348 for line in proc.stdout:
349 match = git_svn_re.match(line)
350 if match:
351 url = match.group(1)
352 proc.stdout.close() # Cut pipe.
353 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000354
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000355 if url:
356 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
357 remotes = RunGit(['config', '--get-regexp',
358 r'^svn-remote\..*\.url']).splitlines()
359 for remote in remotes:
360 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000361 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 remote = match.group(1)
363 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000364 rewrite_root = RunGit(
365 ['config', 'svn-remote.%s.rewriteRoot' % remote],
366 error_ok=True).strip()
367 if rewrite_root:
368 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000369 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000370 ['config', 'svn-remote.%s.fetch' % remote],
371 error_ok=True).strip()
372 if fetch_spec:
373 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
374 if self.svn_branch:
375 break
376 branch_spec = RunGit(
377 ['config', 'svn-remote.%s.branches' % remote],
378 error_ok=True).strip()
379 if branch_spec:
380 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
381 if self.svn_branch:
382 break
383 tag_spec = RunGit(
384 ['config', 'svn-remote.%s.tags' % remote],
385 error_ok=True).strip()
386 if tag_spec:
387 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
388 if self.svn_branch:
389 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000390
391 if not self.svn_branch:
392 DieWithError('Can\'t guess svn branch -- try specifying it on the '
393 'command line')
394
395 return self.svn_branch
396
397 def GetTreeStatusUrl(self, error_ok=False):
398 if not self.tree_status_url:
399 error_message = ('You must configure your tree status URL by running '
400 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000401 self.tree_status_url = self._GetRietveldConfig(
402 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403 return self.tree_status_url
404
405 def GetViewVCUrl(self):
406 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000407 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000408 return self.viewvc_url
409
rmistry@google.com90752582014-01-14 21:04:50 +0000410 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000411 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000412
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000413 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000414 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000415
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000416 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000417 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000418
ukai@chromium.orge8077812012-02-03 03:41:46 +0000419 def GetIsGerrit(self):
420 """Return true if this repo is assosiated with gerrit code review system."""
421 if self.is_gerrit is None:
422 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
423 return self.is_gerrit
424
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000425 def GetGitEditor(self):
426 """Return the editor specified in the git config, or None if none is."""
427 if self.git_editor is None:
428 self.git_editor = self._GetConfig('core.editor', error_ok=True)
429 return self.git_editor or None
430
thestig@chromium.org44202a22014-03-11 19:22:18 +0000431 def GetLintRegex(self):
432 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
433 DEFAULT_LINT_REGEX)
434
435 def GetLintIgnoreRegex(self):
436 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
437 DEFAULT_LINT_IGNORE_REGEX)
438
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000439 def _GetRietveldConfig(self, param, **kwargs):
440 return self._GetConfig('rietveld.' + param, **kwargs)
441
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000442 def _GetConfig(self, param, **kwargs):
443 self.LazyUpdateIfNeeded()
444 return RunGit(['config', param], **kwargs).strip()
445
446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000447def ShortBranchName(branch):
448 """Convert a name like 'refs/heads/foo' to just 'foo'."""
449 return branch.replace('refs/heads/', '')
450
451
452class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000453 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000454 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000455 global settings
456 if not settings:
457 # Happens when git_cl.py is used as a utility library.
458 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459 settings.GetDefaultServerUrl()
460 self.branchref = branchref
461 if self.branchref:
462 self.branch = ShortBranchName(self.branchref)
463 else:
464 self.branch = None
465 self.rietveld_server = None
466 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000467 self.lookedup_issue = False
468 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469 self.has_description = False
470 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000471 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000473 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000474 self.cc = None
475 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000476 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000477 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000478
479 def GetCCList(self):
480 """Return the users cc'd on this CL.
481
482 Return is a string suitable for passing to gcl with the --cc flag.
483 """
484 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000485 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000486 more_cc = ','.join(self.watchers)
487 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
488 return self.cc
489
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000490 def GetCCListWithoutDefault(self):
491 """Return the users cc'd on this CL excluding default ones."""
492 if self.cc is None:
493 self.cc = ','.join(self.watchers)
494 return self.cc
495
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000496 def SetWatchers(self, watchers):
497 """Set the list of email addresses that should be cc'd based on the changed
498 files in this CL.
499 """
500 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000501
502 def GetBranch(self):
503 """Returns the short branch name, e.g. 'master'."""
504 if not self.branch:
505 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
506 self.branch = ShortBranchName(self.branchref)
507 return self.branch
508
509 def GetBranchRef(self):
510 """Returns the full branch name, e.g. 'refs/heads/master'."""
511 self.GetBranch() # Poke the lazy loader.
512 return self.branchref
513
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000514 @staticmethod
515 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000516 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000517 e.g. 'origin', 'refs/heads/master'
518 """
519 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000520 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
521 error_ok=True).strip()
522 if upstream_branch:
523 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
524 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000525 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
526 error_ok=True).strip()
527 if upstream_branch:
528 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000529 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000530 # Fall back on trying a git-svn upstream branch.
531 if settings.GetIsGitSvn():
532 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000533 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000534 # Else, try to guess the origin remote.
535 remote_branches = RunGit(['branch', '-r']).split()
536 if 'origin/master' in remote_branches:
537 # Fall back on origin/master if it exits.
538 remote = 'origin'
539 upstream_branch = 'refs/heads/master'
540 elif 'origin/trunk' in remote_branches:
541 # Fall back on origin/trunk if it exists. Generally a shared
542 # git-svn clone
543 remote = 'origin'
544 upstream_branch = 'refs/heads/trunk'
545 else:
546 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000547Either pass complete "git diff"-style arguments, like
548 git cl upload origin/master
549or verify this branch is set up to track another (via the --track argument to
550"git checkout -b ...").""")
551
552 return remote, upstream_branch
553
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000554 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000555 return git_common.get_or_create_merge_base(self.GetBranch(),
556 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000557
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000558 def GetUpstreamBranch(self):
559 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000560 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000561 if remote is not '.':
562 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
563 self.upstream_branch = upstream_branch
564 return self.upstream_branch
565
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000566 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000567 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000568 remote, branch = None, self.GetBranch()
569 seen_branches = set()
570 while branch not in seen_branches:
571 seen_branches.add(branch)
572 remote, branch = self.FetchUpstreamTuple(branch)
573 branch = ShortBranchName(branch)
574 if remote != '.' or branch.startswith('refs/remotes'):
575 break
576 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000577 remotes = RunGit(['remote'], error_ok=True).split()
578 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000579 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000580 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000581 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000582 logging.warning('Could not determine which remote this change is '
583 'associated with, so defaulting to "%s". This may '
584 'not be what you want. You may prevent this message '
585 'by running "git svn info" as documented here: %s',
586 self._remote,
587 GIT_INSTRUCTIONS_URL)
588 else:
589 logging.warn('Could not determine which remote this change is '
590 'associated with. You may prevent this message by '
591 'running "git svn info" as documented here: %s',
592 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000593 branch = 'HEAD'
594 if branch.startswith('refs/remotes'):
595 self._remote = (remote, branch)
596 else:
597 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000598 return self._remote
599
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000600 def GitSanityChecks(self, upstream_git_obj):
601 """Checks git repo status and ensures diff is from local commits."""
602
603 # Verify the commit we're diffing against is in our current branch.
604 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
605 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
606 if upstream_sha != common_ancestor:
607 print >> sys.stderr, (
608 'ERROR: %s is not in the current branch. You may need to rebase '
609 'your tracking branch' % upstream_sha)
610 return False
611
612 # List the commits inside the diff, and verify they are all local.
613 commits_in_diff = RunGit(
614 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
615 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
616 remote_branch = remote_branch.strip()
617 if code != 0:
618 _, remote_branch = self.GetRemoteBranch()
619
620 commits_in_remote = RunGit(
621 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
622
623 common_commits = set(commits_in_diff) & set(commits_in_remote)
624 if common_commits:
625 print >> sys.stderr, (
626 'ERROR: Your diff contains %d commits already in %s.\n'
627 'Run "git log --oneline %s..HEAD" to get a list of commits in '
628 'the diff. If you are using a custom git flow, you can override'
629 ' the reference used for this check with "git config '
630 'gitcl.remotebranch <git-ref>".' % (
631 len(common_commits), remote_branch, upstream_git_obj))
632 return False
633 return True
634
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000635 def GetGitBaseUrlFromConfig(self):
636 """Return the configured base URL from branch.<branchname>.baseurl.
637
638 Returns None if it is not set.
639 """
640 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
641 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000642
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 def GetRemoteUrl(self):
644 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
645
646 Returns None if there is no remote.
647 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000648 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
650
651 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000652 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000653 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000654 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000655 self.issue = int(issue) or None if issue else None
656 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.issue
658
659 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000660 if not self.rietveld_server:
661 # If we're on a branch then get the server potentially associated
662 # with that branch.
663 if self.GetIssue():
664 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
665 ['config', self._RietveldServer()], error_ok=True).strip())
666 if not self.rietveld_server:
667 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668 return self.rietveld_server
669
670 def GetIssueURL(self):
671 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000672 if not self.GetIssue():
673 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
675
676 def GetDescription(self, pretty=False):
677 if not self.has_description:
678 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000679 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000680 try:
681 self.description = self.RpcServer().get_description(issue).strip()
682 except urllib2.HTTPError, e:
683 if e.code == 404:
684 DieWithError(
685 ('\nWhile fetching the description for issue %d, received a '
686 '404 (not found)\n'
687 'error. It is likely that you deleted this '
688 'issue on the server. If this is the\n'
689 'case, please run\n\n'
690 ' git cl issue 0\n\n'
691 'to clear the association with the deleted issue. Then run '
692 'this command again.') % issue)
693 else:
694 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000695 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 self.has_description = True
697 if pretty:
698 wrapper = textwrap.TextWrapper()
699 wrapper.initial_indent = wrapper.subsequent_indent = ' '
700 return wrapper.fill(self.description)
701 return self.description
702
703 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000704 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000705 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706 patchset = RunGit(['config', self._PatchsetSetting()],
707 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000708 self.patchset = int(patchset) or None if patchset else None
709 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710 return self.patchset
711
712 def SetPatchset(self, patchset):
713 """Set this branch's patchset. If patchset=0, clears the patchset."""
714 if patchset:
715 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000716 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 else:
718 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000719 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000720 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000722 def GetMostRecentPatchset(self):
723 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000724
725 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000726 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000727 '/download/issue%s_%s.diff' % (issue, patchset))
728
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000729 def GetIssueProperties(self):
730 if self._props is None:
731 issue = self.GetIssue()
732 if not issue:
733 self._props = {}
734 else:
735 self._props = self.RpcServer().get_issue_properties(issue, True)
736 return self._props
737
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000738 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000739 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 def SetIssue(self, issue):
742 """Set this branch's issue. If issue=0, clears the issue."""
743 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000744 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 RunGit(['config', self._IssueSetting(), str(issue)])
746 if self.rietveld_server:
747 RunGit(['config', self._RietveldServer(), self.rietveld_server])
748 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000749 current_issue = self.GetIssue()
750 if current_issue:
751 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000752 self.issue = None
753 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000755 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000756 if not self.GitSanityChecks(upstream_branch):
757 DieWithError('\nGit sanity check failure')
758
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000760 if not root:
761 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000762 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000763
764 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000766 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000767 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000768 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000769 except subprocess2.CalledProcessError:
770 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000771 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000772 'This branch probably doesn\'t exist anymore. To reset the\n'
773 'tracking branch, please run\n'
774 ' git branch --set-upstream %s trunk\n'
775 'replacing trunk with origin/master or the relevant branch') %
776 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000777
maruel@chromium.org52424302012-08-29 15:14:30 +0000778 issue = self.GetIssue()
779 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000780 if issue:
781 description = self.GetDescription()
782 else:
783 # If the change was never uploaded, use the log messages of all commits
784 # up to the branch point, as git cl upload will prefill the description
785 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000786 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
787 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000788
789 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000790 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000791 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000792 name,
793 description,
794 absroot,
795 files,
796 issue,
797 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000798 author,
799 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000800
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000801 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000802 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000803
804 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000805 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000806 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000807 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000808 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000809 except presubmit_support.PresubmitFailure, e:
810 DieWithError(
811 ('%s\nMaybe your depot_tools is out of date?\n'
812 'If all fails, contact maruel@') % e)
813
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000814 def UpdateDescription(self, description):
815 self.description = description
816 return self.RpcServer().update_description(
817 self.GetIssue(), self.description)
818
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000820 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000821 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000823 def SetFlag(self, flag, value):
824 """Patchset must match."""
825 if not self.GetPatchset():
826 DieWithError('The patchset needs to match. Send another patchset.')
827 try:
828 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000829 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000830 except urllib2.HTTPError, e:
831 if e.code == 404:
832 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
833 if e.code == 403:
834 DieWithError(
835 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
836 'match?') % (self.GetIssue(), self.GetPatchset()))
837 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000839 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840 """Returns an upload.RpcServer() to access this review's rietveld instance.
841 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000842 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000843 self._rpc_server = rietveld.CachingRietveld(
844 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000845 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846
847 def _IssueSetting(self):
848 """Return the git setting that stores this change's issue."""
849 return 'branch.%s.rietveldissue' % self.GetBranch()
850
851 def _PatchsetSetting(self):
852 """Return the git setting that stores this change's most recent patchset."""
853 return 'branch.%s.rietveldpatchset' % self.GetBranch()
854
855 def _RietveldServer(self):
856 """Returns the git setting that stores this change's rietveld server."""
857 return 'branch.%s.rietveldserver' % self.GetBranch()
858
859
860def GetCodereviewSettingsInteractively():
861 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000862 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 server = settings.GetDefaultServerUrl(error_ok=True)
864 prompt = 'Rietveld server (host[:port])'
865 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000866 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 if not server and not newserver:
868 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000869 if newserver:
870 newserver = gclient_utils.UpgradeToHttps(newserver)
871 if newserver != server:
872 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000874 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 prompt = caption
876 if initial:
877 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000878 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 if new_val == 'x':
880 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000881 elif new_val:
882 if is_url:
883 new_val = gclient_utils.UpgradeToHttps(new_val)
884 if new_val != initial:
885 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000887 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000888 SetProperty(settings.GetDefaultPrivateFlag(),
889 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000891 'tree-status-url', False)
892 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000893 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894
895 # TODO: configure a default branch to diff against, rather than this
896 # svn-based hackery.
897
898
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000899class ChangeDescription(object):
900 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000901 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000902 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000903
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000904 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000905 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000906
agable@chromium.org42c20792013-09-12 17:34:49 +0000907 @property # www.logilab.org/ticket/89786
908 def description(self): # pylint: disable=E0202
909 return '\n'.join(self._description_lines)
910
911 def set_description(self, desc):
912 if isinstance(desc, basestring):
913 lines = desc.splitlines()
914 else:
915 lines = [line.rstrip() for line in desc]
916 while lines and not lines[0]:
917 lines.pop(0)
918 while lines and not lines[-1]:
919 lines.pop(-1)
920 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000921
922 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000923 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000924 assert isinstance(reviewers, list), reviewers
925 if not reviewers:
926 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000927 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000928
agable@chromium.org42c20792013-09-12 17:34:49 +0000929 # Get the set of R= and TBR= lines and remove them from the desciption.
930 regexp = re.compile(self.R_LINE)
931 matches = [regexp.match(line) for line in self._description_lines]
932 new_desc = [l for i, l in enumerate(self._description_lines)
933 if not matches[i]]
934 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000935
agable@chromium.org42c20792013-09-12 17:34:49 +0000936 # Construct new unified R= and TBR= lines.
937 r_names = []
938 tbr_names = []
939 for match in matches:
940 if not match:
941 continue
942 people = cleanup_list([match.group(2).strip()])
943 if match.group(1) == 'TBR':
944 tbr_names.extend(people)
945 else:
946 r_names.extend(people)
947 for name in r_names:
948 if name not in reviewers:
949 reviewers.append(name)
950 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
951 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
952
953 # Put the new lines in the description where the old first R= line was.
954 line_loc = next((i for i, match in enumerate(matches) if match), -1)
955 if 0 <= line_loc < len(self._description_lines):
956 if new_tbr_line:
957 self._description_lines.insert(line_loc, new_tbr_line)
958 if new_r_line:
959 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000960 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000961 if new_r_line:
962 self.append_footer(new_r_line)
963 if new_tbr_line:
964 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000965
966 def prompt(self):
967 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000968 self.set_description([
969 '# Enter a description of the change.',
970 '# This will be displayed on the codereview site.',
971 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000972 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000973 '--------------------',
974 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000975
agable@chromium.org42c20792013-09-12 17:34:49 +0000976 regexp = re.compile(self.BUG_LINE)
977 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000978 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000979 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000980 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000981 if not content:
982 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000983 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000984
985 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000986 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
987 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000988 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000989 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000990
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000991 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000992 if self._description_lines:
993 # Add an empty line if either the last line or the new line isn't a tag.
994 last_line = self._description_lines[-1]
995 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
996 not presubmit_support.Change.TAG_LINE_RE.match(line)):
997 self._description_lines.append('')
998 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000999
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001000 def get_reviewers(self):
1001 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001002 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1003 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001004 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001005
1006
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001007def get_approving_reviewers(props):
1008 """Retrieves the reviewers that approved a CL from the issue properties with
1009 messages.
1010
1011 Note that the list may contain reviewers that are not committer, thus are not
1012 considered by the CQ.
1013 """
1014 return sorted(
1015 set(
1016 message['sender']
1017 for message in props['messages']
1018 if message['approval'] and message['sender'] in props['reviewers']
1019 )
1020 )
1021
1022
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023def FindCodereviewSettingsFile(filename='codereview.settings'):
1024 """Finds the given file starting in the cwd and going up.
1025
1026 Only looks up to the top of the repository unless an
1027 'inherit-review-settings-ok' file exists in the root of the repository.
1028 """
1029 inherit_ok_file = 'inherit-review-settings-ok'
1030 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001031 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001032 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1033 root = '/'
1034 while True:
1035 if filename in os.listdir(cwd):
1036 if os.path.isfile(os.path.join(cwd, filename)):
1037 return open(os.path.join(cwd, filename))
1038 if cwd == root:
1039 break
1040 cwd = os.path.dirname(cwd)
1041
1042
1043def LoadCodereviewSettingsFromFile(fileobj):
1044 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001045 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 def SetProperty(name, setting, unset_error_ok=False):
1048 fullname = 'rietveld.' + name
1049 if setting in keyvals:
1050 RunGit(['config', fullname, keyvals[setting]])
1051 else:
1052 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1053
1054 SetProperty('server', 'CODE_REVIEW_SERVER')
1055 # Only server setting is required. Other settings can be absent.
1056 # In that case, we ignore errors raised during option deletion attempt.
1057 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001058 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1060 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001061 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001062 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1063 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001065 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001066 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1069 #should be of the form
1070 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1071 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1072 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1073 keyvals['ORIGIN_URL_CONFIG']])
1074
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001076def urlretrieve(source, destination):
1077 """urllib is broken for SSL connections via a proxy therefore we
1078 can't use urllib.urlretrieve()."""
1079 with open(destination, 'w') as f:
1080 f.write(urllib2.urlopen(source).read())
1081
1082
ukai@chromium.org712d6102013-11-27 00:52:58 +00001083def hasSheBang(fname):
1084 """Checks fname is a #! script."""
1085 with open(fname) as f:
1086 return f.read(2).startswith('#!')
1087
1088
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001089def DownloadHooks(force):
1090 """downloads hooks
1091
1092 Args:
1093 force: True to update hooks. False to install hooks if not present.
1094 """
1095 if not settings.GetIsGerrit():
1096 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001097 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001098 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1099 if not os.access(dst, os.X_OK):
1100 if os.path.exists(dst):
1101 if not force:
1102 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001103 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001104 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001105 if not hasSheBang(dst):
1106 DieWithError('Not a script: %s\n'
1107 'You need to download from\n%s\n'
1108 'into .git/hooks/commit-msg and '
1109 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001110 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1111 except Exception:
1112 if os.path.exists(dst):
1113 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001114 DieWithError('\nFailed to download hooks.\n'
1115 'You need to download from\n%s\n'
1116 'into .git/hooks/commit-msg and '
1117 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001118
1119
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001120@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001122 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001124 parser.add_option('--activate-update', action='store_true',
1125 help='activate auto-updating [rietveld] section in '
1126 '.git/config')
1127 parser.add_option('--deactivate-update', action='store_true',
1128 help='deactivate auto-updating [rietveld] section in '
1129 '.git/config')
1130 options, args = parser.parse_args(args)
1131
1132 if options.deactivate_update:
1133 RunGit(['config', 'rietveld.autoupdate', 'false'])
1134 return
1135
1136 if options.activate_update:
1137 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1138 return
1139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 if len(args) == 0:
1141 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001142 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 return 0
1144
1145 url = args[0]
1146 if not url.endswith('codereview.settings'):
1147 url = os.path.join(url, 'codereview.settings')
1148
1149 # Load code review settings and download hooks (if available).
1150 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001151 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 return 0
1153
1154
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001155def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001156 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001157 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1158 branch = ShortBranchName(branchref)
1159 _, args = parser.parse_args(args)
1160 if not args:
1161 print("Current base-url:")
1162 return RunGit(['config', 'branch.%s.base-url' % branch],
1163 error_ok=False).strip()
1164 else:
1165 print("Setting base-url to %s" % args[0])
1166 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1167 error_ok=False).strip()
1168
1169
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001171 """Show status of changelists.
1172
1173 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001174 - Red not sent for review or broken
1175 - Blue waiting for review
1176 - Yellow waiting for you to reply to review
1177 - Green LGTM'ed
1178 - Magenta in the commit queue
1179 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001180
1181 Also see 'git cl comments'.
1182 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 parser.add_option('--field',
1184 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001185 parser.add_option('-f', '--fast', action='store_true',
1186 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001188 if args:
1189 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001192 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 if options.field.startswith('desc'):
1194 print cl.GetDescription()
1195 elif options.field == 'id':
1196 issueid = cl.GetIssue()
1197 if issueid:
1198 print issueid
1199 elif options.field == 'patch':
1200 patchset = cl.GetPatchset()
1201 if patchset:
1202 print patchset
1203 elif options.field == 'url':
1204 url = cl.GetIssueURL()
1205 if url:
1206 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001207 return 0
1208
1209 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1210 if not branches:
1211 print('No local branch found.')
1212 return 0
1213
1214 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001215 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001216 alignment = max(5, max(len(b) for b in branches))
1217 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001218 # Adhoc thread pool to request data concurrently.
1219 output = Queue.Queue()
1220
1221 # Silence upload.py otherwise it becomes unweldly.
1222 upload.verbosity = 0
1223
1224 if not options.fast:
1225 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001226 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001227 c = Changelist(branchref=b)
1228 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001229 props = {}
1230 r = None
1231 if i:
1232 try:
1233 props = c.GetIssueProperties()
1234 r = c.GetApprovingReviewers() if i else None
1235 except urllib2.HTTPError:
1236 # The issue probably doesn't exist anymore.
1237 i += ' (broken)'
1238
1239 msgs = props.get('messages') or []
1240
1241 if not i:
1242 color = Fore.WHITE
1243 elif props.get('closed'):
1244 # Issue is closed.
1245 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001246 elif props.get('commit'):
1247 # Issue is in the commit queue.
1248 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001249 elif r:
1250 # Was LGTM'ed.
1251 color = Fore.GREEN
1252 elif not msgs:
1253 # No message was sent.
1254 color = Fore.RED
1255 elif msgs[-1]['sender'] != props.get('owner_email'):
1256 color = Fore.YELLOW
1257 else:
1258 color = Fore.BLUE
1259 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001260
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001261 # Process one branch synchronously to work through authentication, then
1262 # spawn threads to process all the other branches in parallel.
1263 if branches:
1264 fetch(branches[0])
1265 threads = [
1266 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001267 for t in threads:
1268 t.daemon = True
1269 t.start()
1270 else:
1271 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1272 for b in branches:
1273 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001274 url = c.GetIssueURL()
1275 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001276
1277 tmp = {}
1278 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001279 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001280 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001281 b, i, color = output.get()
1282 tmp[b] = (i, color)
1283 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001284 reset = Fore.RESET
1285 if not sys.stdout.isatty():
1286 color = ''
1287 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001288 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001289 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001290
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001291 cl = Changelist()
1292 print
1293 print 'Current branch:',
1294 if not cl.GetIssue():
1295 print 'no issue assigned.'
1296 return 0
1297 print cl.GetBranch()
1298 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1299 print 'Issue description:'
1300 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 return 0
1302
1303
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001304def colorize_CMDstatus_doc():
1305 """To be called once in main() to add colors to git cl status help."""
1306 colors = [i for i in dir(Fore) if i[0].isupper()]
1307
1308 def colorize_line(line):
1309 for color in colors:
1310 if color in line.upper():
1311 # Extract whitespaces first and the leading '-'.
1312 indent = len(line) - len(line.lstrip(' ')) + 1
1313 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1314 return line
1315
1316 lines = CMDstatus.__doc__.splitlines()
1317 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1318
1319
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001320@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001322 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323
1324 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001325 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001326 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327
1328 cl = Changelist()
1329 if len(args) > 0:
1330 try:
1331 issue = int(args[0])
1332 except ValueError:
1333 DieWithError('Pass a number to set the issue or none to list it.\n'
1334 'Maybe you want to run git cl status?')
1335 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001336 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 return 0
1338
1339
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001340def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001341 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001342 (_, args) = parser.parse_args(args)
1343 if args:
1344 parser.error('Unsupported argument: %s' % args)
1345
1346 cl = Changelist()
1347 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001348 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001349 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001350 if message['disapproval']:
1351 color = Fore.RED
1352 elif message['approval']:
1353 color = Fore.GREEN
1354 elif message['sender'] == data['owner_email']:
1355 color = Fore.MAGENTA
1356 else:
1357 color = Fore.BLUE
1358 print '\n%s%s %s%s' % (
1359 color, message['date'].split('.', 1)[0], message['sender'],
1360 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001361 if message['text'].strip():
1362 print '\n'.join(' ' + l for l in message['text'].splitlines())
1363 return 0
1364
1365
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001366def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001367 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001368 cl = Changelist()
1369 if not cl.GetIssue():
1370 DieWithError('This branch has no associated changelist.')
1371 description = ChangeDescription(cl.GetDescription())
1372 description.prompt()
1373 cl.UpdateDescription(description.description)
1374 return 0
1375
1376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377def CreateDescriptionFromLog(args):
1378 """Pulls out the commit log to use as a base for the CL description."""
1379 log_args = []
1380 if len(args) == 1 and not args[0].endswith('.'):
1381 log_args = [args[0] + '..']
1382 elif len(args) == 1 and args[0].endswith('...'):
1383 log_args = [args[0][:-1]]
1384 elif len(args) == 2:
1385 log_args = [args[0] + '..' + args[1]]
1386 else:
1387 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001388 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389
1390
thestig@chromium.org44202a22014-03-11 19:22:18 +00001391def CMDlint(parser, args):
1392 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001393 parser.add_option('--filter', action='append', metavar='-x,+y',
1394 help='Comma-separated list of cpplint\'s category-filters')
1395 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001396
1397 # Access to a protected member _XX of a client class
1398 # pylint: disable=W0212
1399 try:
1400 import cpplint
1401 import cpplint_chromium
1402 except ImportError:
1403 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1404 return 1
1405
1406 # Change the current working directory before calling lint so that it
1407 # shows the correct base.
1408 previous_cwd = os.getcwd()
1409 os.chdir(settings.GetRoot())
1410 try:
1411 cl = Changelist()
1412 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1413 files = [f.LocalPath() for f in change.AffectedFiles()]
1414
1415 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001416 command = args + files
1417 if options.filter:
1418 command = ['--filter=' + ','.join(options.filter)] + command
1419 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001420
1421 white_regex = re.compile(settings.GetLintRegex())
1422 black_regex = re.compile(settings.GetLintIgnoreRegex())
1423 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1424 for filename in filenames:
1425 if white_regex.match(filename):
1426 if black_regex.match(filename):
1427 print "Ignoring file %s" % filename
1428 else:
1429 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1430 extra_check_functions)
1431 else:
1432 print "Skipping file %s" % filename
1433 finally:
1434 os.chdir(previous_cwd)
1435 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1436 if cpplint._cpplint_state.error_count != 0:
1437 return 1
1438 return 0
1439
1440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001442 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001443 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001445 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001446 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 (options, args) = parser.parse_args(args)
1448
ukai@chromium.org259e4682012-10-25 07:36:33 +00001449 if not options.force and is_dirty_git_tree('presubmit'):
1450 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451 return 1
1452
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001453 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 if args:
1455 base_branch = args[0]
1456 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001457 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001458 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001460 cl.RunHook(
1461 committing=not options.upload,
1462 may_prompt=False,
1463 verbose=options.verbose,
1464 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001465 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466
1467
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001468def AddChangeIdToCommitMessage(options, args):
1469 """Re-commits using the current message, assumes the commit hook is in
1470 place.
1471 """
1472 log_desc = options.message or CreateDescriptionFromLog(args)
1473 git_command = ['commit', '--amend', '-m', log_desc]
1474 RunGit(git_command)
1475 new_log_desc = CreateDescriptionFromLog(args)
1476 if CHANGE_ID in new_log_desc:
1477 print 'git-cl: Added Change-Id to commit message.'
1478 else:
1479 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1480
1481
ukai@chromium.orge8077812012-02-03 03:41:46 +00001482def GerritUpload(options, args, cl):
1483 """upload the current branch to gerrit."""
1484 # We assume the remote called "origin" is the one we want.
1485 # It is probably not worthwhile to support different workflows.
1486 remote = 'origin'
1487 branch = 'master'
1488 if options.target_branch:
1489 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001491 change_desc = ChangeDescription(
1492 options.message or CreateDescriptionFromLog(args))
1493 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001494 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001496 if CHANGE_ID not in change_desc.description:
1497 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001498
1499 commits = RunGit(['rev-list', '%s/%s...' % (remote, branch)]).splitlines()
1500 if len(commits) > 1:
1501 print('WARNING: This will upload %d commits. Run the following command '
1502 'to see which commits will be uploaded: ' % len(commits))
1503 print('git log %s/%s...' % (remote, branch))
1504 print('You can also use `git squash-branch` to squash these into a single'
1505 'commit.')
1506 ask_for_data('About to upload; enter to confirm.')
1507
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001508 if options.reviewers:
1509 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510
ukai@chromium.orge8077812012-02-03 03:41:46 +00001511 receive_options = []
1512 cc = cl.GetCCList().split(',')
1513 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001514 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001515 cc = filter(None, cc)
1516 if cc:
1517 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001518 if change_desc.get_reviewers():
1519 receive_options.extend(
1520 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521
ukai@chromium.orge8077812012-02-03 03:41:46 +00001522 git_command = ['push']
1523 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001524 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001525 ' '.join(receive_options))
1526 git_command += [remote, 'HEAD:refs/for/' + branch]
1527 RunGit(git_command)
1528 # TODO(ukai): parse Change-Id: and set issue number?
1529 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001530
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531
ukai@chromium.orge8077812012-02-03 03:41:46 +00001532def RietveldUpload(options, args, cl):
1533 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1535 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 if options.emulate_svn_auto_props:
1537 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001538
1539 change_desc = None
1540
pgervais@chromium.org91141372014-01-09 23:27:20 +00001541 if options.email is not None:
1542 upload_args.extend(['--email', options.email])
1543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001545 if options.title:
1546 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001547 if options.message:
1548 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001549 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 print ("This branch is associated with issue %s. "
1551 "Adding patch to that issue." % cl.GetIssue())
1552 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001553 if options.title:
1554 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001555 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001556 change_desc = ChangeDescription(message)
1557 if options.reviewers:
1558 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001559 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001560 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001561
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001562 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 print "Description is empty; aborting."
1564 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001565
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001566 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001567 if change_desc.get_reviewers():
1568 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001569 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001570 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001571 DieWithError("Must specify reviewers to send email.")
1572 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001573
1574 # We check this before applying rietveld.private assuming that in
1575 # rietveld.cc only addresses which we can send private CLs to are listed
1576 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1577 # --private is specified explicitly on the command line.
1578 if options.private:
1579 logging.warn('rietveld.cc is ignored since private flag is specified. '
1580 'You need to review and add them manually if necessary.')
1581 cc = cl.GetCCListWithoutDefault()
1582 else:
1583 cc = cl.GetCCList()
1584 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001585 if cc:
1586 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001588 if options.private or settings.GetDefaultPrivateFlag() == "True":
1589 upload_args.append('--private')
1590
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001591 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001592 if not options.find_copies:
1593 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001594
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001595 # Include the upstream repo's URL in the change -- this is useful for
1596 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001597 remote_url = cl.GetGitBaseUrlFromConfig()
1598 if not remote_url:
1599 if settings.GetIsGitSvn():
1600 # URL is dependent on the current directory.
1601 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1602 if data:
1603 keys = dict(line.split(': ', 1) for line in data.splitlines()
1604 if ': ' in line)
1605 remote_url = keys.get('URL', None)
1606 else:
1607 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1608 remote_url = (cl.GetRemoteUrl() + '@'
1609 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610 if remote_url:
1611 upload_args.extend(['--base_url', remote_url])
1612
1613 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001614 upload_args = ['upload'] + upload_args + args
1615 logging.info('upload.RealMain(%s)', upload_args)
1616 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001617 issue = int(issue)
1618 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001619 except KeyboardInterrupt:
1620 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621 except:
1622 # If we got an exception after the user typed a description for their
1623 # change, back up the description before re-raising.
1624 if change_desc:
1625 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1626 print '\nGot exception while uploading -- saving description to %s\n' \
1627 % backup_path
1628 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001629 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630 backup_file.close()
1631 raise
1632
1633 if not cl.GetIssue():
1634 cl.SetIssue(issue)
1635 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001636
1637 if options.use_commit_queue:
1638 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 return 0
1640
1641
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001642def cleanup_list(l):
1643 """Fixes a list so that comma separated items are put as individual items.
1644
1645 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1646 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1647 """
1648 items = sum((i.split(',') for i in l), [])
1649 stripped_items = (i.strip() for i in items)
1650 return sorted(filter(None, stripped_items))
1651
1652
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001653@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001654def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001655 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001656 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1657 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001658 parser.add_option('--bypass-watchlists', action='store_true',
1659 dest='bypass_watchlists',
1660 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001661 parser.add_option('-f', action='store_true', dest='force',
1662 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001663 parser.add_option('-m', dest='message', help='message for patchset')
1664 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001665 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001666 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001667 help='reviewer email addresses')
1668 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001669 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001670 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001671 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001672 help='send email to reviewer immediately')
1673 parser.add_option("--emulate_svn_auto_props", action="store_true",
1674 dest="emulate_svn_auto_props",
1675 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001676 parser.add_option('-c', '--use-commit-queue', action='store_true',
1677 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001678 parser.add_option('--private', action='store_true',
1679 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001680 parser.add_option('--target_branch',
1681 help='When uploading to gerrit, remote branch to '
1682 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001683 parser.add_option('--email', default=None,
1684 help='email address to use to connect to Rietveld')
1685
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001686 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001687 (options, args) = parser.parse_args(args)
1688
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001689 if options.target_branch and not settings.GetIsGerrit():
1690 parser.error('Use --target_branch for non gerrit repository.')
1691
ukai@chromium.org259e4682012-10-25 07:36:33 +00001692 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001693 return 1
1694
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001695 options.reviewers = cleanup_list(options.reviewers)
1696 options.cc = cleanup_list(options.cc)
1697
ukai@chromium.orge8077812012-02-03 03:41:46 +00001698 cl = Changelist()
1699 if args:
1700 # TODO(ukai): is it ok for gerrit case?
1701 base_branch = args[0]
1702 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001703 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001704 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001705 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001706
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001707 # Apply watchlists on upload.
1708 change = cl.GetChange(base_branch, None)
1709 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1710 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001711 if not options.bypass_watchlists:
1712 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001713
ukai@chromium.orge8077812012-02-03 03:41:46 +00001714 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001715 if options.reviewers:
1716 # Set the reviewer list now so that presubmit checks can access it.
1717 change_description = ChangeDescription(change.FullDescriptionText())
1718 change_description.update_reviewers(options.reviewers)
1719 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001720 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001721 may_prompt=not options.force,
1722 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001723 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001724 if not hook_results.should_continue():
1725 return 1
1726 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001727 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001728
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001729 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001730 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001731 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001732 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001733 print ('The last upload made from this repository was patchset #%d but '
1734 'the most recent patchset on the server is #%d.'
1735 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001736 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1737 'from another machine or branch the patch you\'re uploading now '
1738 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001739 ask_for_data('About to upload; enter to confirm.')
1740
iannucci@chromium.org79540052012-10-19 23:15:26 +00001741 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001742 if settings.GetIsGerrit():
1743 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001744 ret = RietveldUpload(options, args, cl)
1745 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001746 git_set_branch_value('last-upload-hash',
1747 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001748
1749 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001750
1751
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001752def IsSubmoduleMergeCommit(ref):
1753 # When submodules are added to the repo, we expect there to be a single
1754 # non-git-svn merge commit at remote HEAD with a signature comment.
1755 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001756 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001757 return RunGit(cmd) != ''
1758
1759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760def SendUpstream(parser, args, cmd):
1761 """Common code for CmdPush and CmdDCommit
1762
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001763 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764 Updates changelog with metadata (e.g. pointer to review).
1765 Pushes/dcommits the code upstream.
1766 Updates review and closes.
1767 """
1768 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1769 help='bypass upload presubmit hook')
1770 parser.add_option('-m', dest='message',
1771 help="override review description")
1772 parser.add_option('-f', action='store_true', dest='force',
1773 help="force yes to questions (don't prompt)")
1774 parser.add_option('-c', dest='contributor',
1775 help="external contributor for patch (appended to " +
1776 "description and used as author for git). Should be " +
1777 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001778 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 (options, args) = parser.parse_args(args)
1780 cl = Changelist()
1781
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001782 current = cl.GetBranch()
1783 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1784 if not settings.GetIsGitSvn() and remote == '.':
1785 print
1786 print 'Attempting to push branch %r into another local branch!' % current
1787 print
1788 print 'Either reparent this branch on top of origin/master:'
1789 print ' git reparent-branch --root'
1790 print
1791 print 'OR run `git rebase-update` if you think the parent branch is already'
1792 print 'committed.'
1793 print
1794 print ' Current parent: %r' % upstream_branch
1795 return 1
1796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797 if not args or cmd == 'push':
1798 # Default to merging against our best guess of the upstream branch.
1799 args = [cl.GetUpstreamBranch()]
1800
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001801 if options.contributor:
1802 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1803 print "Please provide contibutor as 'First Last <email@example.com>'"
1804 return 1
1805
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001807 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808
ukai@chromium.org259e4682012-10-25 07:36:33 +00001809 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810 return 1
1811
1812 # This rev-list syntax means "show all commits not in my branch that
1813 # are in base_branch".
1814 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1815 base_branch]).splitlines()
1816 if upstream_commits:
1817 print ('Base branch "%s" has %d commits '
1818 'not in this branch.' % (base_branch, len(upstream_commits)))
1819 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1820 return 1
1821
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001822 # This is the revision `svn dcommit` will commit on top of.
1823 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1824 '--pretty=format:%H'])
1825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001826 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001827 # If the base_head is a submodule merge commit, the first parent of the
1828 # base_head should be a git-svn commit, which is what we're interested in.
1829 base_svn_head = base_branch
1830 if base_has_submodules:
1831 base_svn_head += '^1'
1832
1833 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834 if extra_commits:
1835 print ('This branch has %d additional commits not upstreamed yet.'
1836 % len(extra_commits.splitlines()))
1837 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1838 'before attempting to %s.' % (base_branch, cmd))
1839 return 1
1840
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001841 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001842 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001843 author = None
1844 if options.contributor:
1845 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001846 hook_results = cl.RunHook(
1847 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001848 may_prompt=not options.force,
1849 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001850 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001851 if not hook_results.should_continue():
1852 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853
1854 if cmd == 'dcommit':
1855 # Check the tree status if the tree status URL is set.
1856 status = GetTreeStatus()
1857 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001858 print('The tree is closed. Please wait for it to reopen. Use '
1859 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 return 1
1861 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001862 print('Unable to determine tree status. Please verify manually and '
1863 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001864 else:
1865 breakpad.SendStack(
1866 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001867 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1868 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001869 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001870
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001871 change_desc = ChangeDescription(options.message)
1872 if not change_desc.description and cl.GetIssue():
1873 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001875 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001876 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001877 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001878 else:
1879 print 'No description set.'
1880 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1881 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001882
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001883 # Keep a separate copy for the commit message, because the commit message
1884 # contains the link to the Rietveld issue, while the Rietveld message contains
1885 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001886 # Keep a separate copy for the commit message.
1887 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001888 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001889
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001890 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001891 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001892 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001894 commit_desc.append_footer('Patch from %s.' % options.contributor)
1895
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001896 print('Description:')
1897 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898
1899 branches = [base_branch, cl.GetBranchRef()]
1900 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001901 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001902 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001904 # We want to squash all this branch's commits into one commit with the proper
1905 # description. We do this by doing a "reset --soft" to the base branch (which
1906 # keeps the working copy the same), then dcommitting that. If origin/master
1907 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1908 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001910 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1911 # Delete the branches if they exist.
1912 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1913 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1914 result = RunGitWithCode(showref_cmd)
1915 if result[0] == 0:
1916 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001917
1918 # We might be in a directory that's present in this branch but not in the
1919 # trunk. Move up to the top of the tree so that git commands that expect a
1920 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001921 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922 if rel_base_path:
1923 os.chdir(rel_base_path)
1924
1925 # Stuff our change into the merge branch.
1926 # We wrap in a try...finally block so if anything goes wrong,
1927 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001928 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001930 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1931 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001933 RunGit(
1934 [
1935 'commit', '--author', options.contributor,
1936 '-m', commit_desc.description,
1937 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001939 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001940 if base_has_submodules:
1941 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1942 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1943 RunGit(['checkout', CHERRY_PICK_BRANCH])
1944 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945 if cmd == 'push':
1946 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001947 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948 retcode, output = RunGitWithCode(
1949 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1950 logging.debug(output)
1951 else:
1952 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001953 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001954 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001955 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001956 finally:
1957 # And then swap back to the original branch and clean up.
1958 RunGit(['checkout', '-q', cl.GetBranch()])
1959 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001960 if base_has_submodules:
1961 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962
1963 if cl.GetIssue():
1964 if cmd == 'dcommit' and 'Committed r' in output:
1965 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1966 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001967 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1968 for l in output.splitlines(False))
1969 match = filter(None, match)
1970 if len(match) != 1:
1971 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1972 output)
1973 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 else:
1975 return 1
1976 viewvc_url = settings.GetViewVCUrl()
1977 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001978 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001979 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001980 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981 print ('Closing issue '
1982 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001983 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001984 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001985 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001986 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001987 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001988 if options.bypass_hooks:
1989 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1990 else:
1991 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001992 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001993 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001994
1995 if retcode == 0:
1996 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1997 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001998 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001999
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000 return 0
2001
2002
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002003@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002005 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002007 message = """This doesn't appear to be an SVN repository.
2008If your project has a git mirror with an upstream SVN master, you probably need
2009to run 'git svn init', see your project's git mirror documentation.
2010If your project has a true writeable upstream repository, you probably want
2011to run 'git cl push' instead.
2012Choose wisely, if you get this wrong, your commit might appear to succeed but
2013will instead be silently ignored."""
2014 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002015 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016 return SendUpstream(parser, args, 'dcommit')
2017
2018
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002019@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002021 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022 if settings.GetIsGitSvn():
2023 print('This appears to be an SVN repository.')
2024 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002025 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 return SendUpstream(parser, args, 'push')
2027
2028
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002029@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002031 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032 parser.add_option('-b', dest='newbranch',
2033 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002034 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002035 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002036 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2037 help='Change to the directory DIR immediately, '
2038 'before doing anything else.')
2039 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002040 help='failed patches spew .rej files rather than '
2041 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2043 help="don't commit after patch applies")
2044 (options, args) = parser.parse_args(args)
2045 if len(args) != 1:
2046 parser.print_help()
2047 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002048 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002050 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002051 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002052
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002053 if options.newbranch:
2054 if options.force:
2055 RunGit(['branch', '-D', options.newbranch],
2056 stderr=subprocess2.PIPE, error_ok=True)
2057 RunGit(['checkout', '-b', options.newbranch,
2058 Changelist().GetUpstreamBranch()])
2059
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002060 return PatchIssue(issue_arg, options.reject, options.nocommit,
2061 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002062
2063
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002064def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002065 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002066 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002067 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002068 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002069 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002070 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002072 # Assume it's a URL to the patch. Default to https.
2073 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002074 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002075 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076 DieWithError('Must pass an issue ID or full URL for '
2077 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002078 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002079 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002080 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002081
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002082 # Switch up to the top-level directory, if necessary, in preparation for
2083 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002084 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002085 if top:
2086 os.chdir(top)
2087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002088 # Git patches have a/ at the beginning of source paths. We strip that out
2089 # with a sed script rather than the -p flag to patch so we can feed either
2090 # Git or svn-style patches into the same apply command.
2091 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002092 try:
2093 patch_data = subprocess2.check_output(
2094 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2095 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096 DieWithError('Git patch mungling failed.')
2097 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099 # We use "git apply" to apply the patch instead of "patch" so that we can
2100 # pick up file adds.
2101 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002102 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002103 if directory:
2104 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002105 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002107 elif IsGitVersionAtLeast('1.7.12'):
2108 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002109 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002110 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002111 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002112 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 DieWithError('Failed to apply the patch')
2114
2115 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002116 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2118 cl = Changelist()
2119 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002120 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002121 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 else:
2123 print "Patch applied to index."
2124 return 0
2125
2126
2127def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002128 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129 # Provide a wrapper for git svn rebase to help avoid accidental
2130 # git svn dcommit.
2131 # It's the only command that doesn't use parser at all since we just defer
2132 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002133
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002134 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002135
2136
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002137def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 """Fetches the tree status and returns either 'open', 'closed',
2139 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002140 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141 if url:
2142 status = urllib2.urlopen(url).read().lower()
2143 if status.find('closed') != -1 or status == '0':
2144 return 'closed'
2145 elif status.find('open') != -1 or status == '1':
2146 return 'open'
2147 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 return 'unset'
2149
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002150
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151def GetTreeStatusReason():
2152 """Fetches the tree status from a json url and returns the message
2153 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002154 url = settings.GetTreeStatusUrl()
2155 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002156 connection = urllib2.urlopen(json_url)
2157 status = json.loads(connection.read())
2158 connection.close()
2159 return status['message']
2160
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002161
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002163 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002164 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165 status = GetTreeStatus()
2166 if 'unset' == status:
2167 print 'You must configure your tree status URL by running "git cl config".'
2168 return 2
2169
2170 print "The tree is %s" % status
2171 print
2172 print GetTreeStatusReason()
2173 if status != 'open':
2174 return 1
2175 return 0
2176
2177
maruel@chromium.org15192402012-09-06 12:38:29 +00002178def CMDtry(parser, args):
2179 """Triggers a try job through Rietveld."""
2180 group = optparse.OptionGroup(parser, "Try job options")
2181 group.add_option(
2182 "-b", "--bot", action="append",
2183 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2184 "times to specify multiple builders. ex: "
2185 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2186 "the try server waterfall for the builders name and the tests "
2187 "available. Can also be used to specify gtest_filter, e.g. "
2188 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2189 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002190 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002191 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002192 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002193 "-r", "--revision",
2194 help="Revision to use for the try job; default: the "
2195 "revision will be determined by the try server; see "
2196 "its waterfall for more info")
2197 group.add_option(
2198 "-c", "--clobber", action="store_true", default=False,
2199 help="Force a clobber before building; e.g. don't do an "
2200 "incremental build")
2201 group.add_option(
2202 "--project",
2203 help="Override which project to use. Projects are defined "
2204 "server-side to define what default bot set to use")
2205 group.add_option(
2206 "-t", "--testfilter", action="append", default=[],
2207 help=("Apply a testfilter to all the selected builders. Unless the "
2208 "builders configurations are similar, use multiple "
2209 "--bot <builder>:<test> arguments."))
2210 group.add_option(
2211 "-n", "--name", help="Try job name; default to current branch name")
2212 parser.add_option_group(group)
2213 options, args = parser.parse_args(args)
2214
2215 if args:
2216 parser.error('Unknown arguments: %s' % args)
2217
2218 cl = Changelist()
2219 if not cl.GetIssue():
2220 parser.error('Need to upload first')
2221
2222 if not options.name:
2223 options.name = cl.GetBranch()
2224
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002225 if options.bot and not options.master:
2226 parser.error('For manually specified bots please also specify the '
2227 'tryserver master, e.g. "-m tryserver.chromium".')
2228
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002229 def GetMasterMap():
2230 # Process --bot and --testfilter.
2231 if not options.bot:
2232 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002233
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002234 # Get try masters from PRESUBMIT.py files.
2235 masters = presubmit_support.DoGetTryMasters(
2236 change,
2237 change.LocalPaths(),
2238 settings.GetRoot(),
2239 None,
2240 None,
2241 options.verbose,
2242 sys.stdout)
2243 if masters:
2244 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002245
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002246 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2247 options.bot = presubmit_support.DoGetTrySlaves(
2248 change,
2249 change.LocalPaths(),
2250 settings.GetRoot(),
2251 None,
2252 None,
2253 options.verbose,
2254 sys.stdout)
2255 if not options.bot:
2256 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002257
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002258 builders_and_tests = {}
2259 # TODO(machenbach): The old style command-line options don't support
2260 # multiple try masters yet.
2261 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2262 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2263
2264 for bot in old_style:
2265 if ':' in bot:
2266 builder, tests = bot.split(':', 1)
2267 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2268 elif ',' in bot:
2269 parser.error('Specify one bot per --bot flag')
2270 else:
2271 builders_and_tests.setdefault(bot, []).append('defaulttests')
2272
2273 for bot, tests in new_style:
2274 builders_and_tests.setdefault(bot, []).extend(tests)
2275
2276 # Return a master map with one master to be backwards compatible. The
2277 # master name defaults to an empty string, which will cause the master
2278 # not to be set on rietveld (deprecated).
2279 return {options.master: builders_and_tests}
2280
2281 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002282
maruel@chromium.org15192402012-09-06 12:38:29 +00002283 if options.testfilter:
2284 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002285 masters = dict((master, dict(
2286 (b, forced_tests) for b, t in slaves.iteritems()
2287 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002288
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002289 for builders in masters.itervalues():
2290 if any('triggered' in b for b in builders):
2291 print >> sys.stderr, (
2292 'ERROR You are trying to send a job to a triggered bot. This type of'
2293 ' bot requires an\ninitial job from a parent (usually a builder). '
2294 'Instead send your job to the parent.\n'
2295 'Bot list: %s' % builders)
2296 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002297
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002298 patchset = cl.GetMostRecentPatchset()
2299 if patchset and patchset != cl.GetPatchset():
2300 print(
2301 '\nWARNING Mismatch between local config and server. Did a previous '
2302 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2303 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002304 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002305 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002306 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002307 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002308 except urllib2.HTTPError, e:
2309 if e.code == 404:
2310 print('404 from rietveld; '
2311 'did you mean to use "git try" instead of "git cl try"?')
2312 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002313 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002314
2315 for (master, builders) in masters.iteritems():
2316 if master:
2317 print 'Master: %s' % master
2318 length = max(len(builder) for builder in builders)
2319 for builder in sorted(builders):
2320 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002321 return 0
2322
2323
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002324@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002325def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002326 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002327 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002328 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002329 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002330
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002331 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002332 if args:
2333 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002334 branch = cl.GetBranch()
2335 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002336 cl = Changelist()
2337 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002338
2339 # Clear configured merge-base, if there is one.
2340 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002341 else:
2342 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002343 return 0
2344
2345
thestig@chromium.org00858c82013-12-02 23:08:03 +00002346def CMDweb(parser, args):
2347 """Opens the current CL in the web browser."""
2348 _, args = parser.parse_args(args)
2349 if args:
2350 parser.error('Unrecognized args: %s' % ' '.join(args))
2351
2352 issue_url = Changelist().GetIssueURL()
2353 if not issue_url:
2354 print >> sys.stderr, 'ERROR No issue to open'
2355 return 1
2356
2357 webbrowser.open(issue_url)
2358 return 0
2359
2360
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002361def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002362 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002363 _, args = parser.parse_args(args)
2364 if args:
2365 parser.error('Unrecognized args: %s' % ' '.join(args))
2366 cl = Changelist()
2367 cl.SetFlag('commit', '1')
2368 return 0
2369
2370
groby@chromium.org411034a2013-02-26 15:12:01 +00002371def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002372 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002373 _, args = parser.parse_args(args)
2374 if args:
2375 parser.error('Unrecognized args: %s' % ' '.join(args))
2376 cl = Changelist()
2377 # Ensure there actually is an issue to close.
2378 cl.GetDescription()
2379 cl.CloseIssue()
2380 return 0
2381
2382
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002383def CMDdiff(parser, args):
2384 """shows differences between local tree and last upload."""
2385 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002386 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002387 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002388 if not issue:
2389 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002390 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002391 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002392
2393 # Create a new branch based on the merge-base
2394 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2395 try:
2396 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002397 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002398 if rtn != 0:
2399 return rtn
2400
2401 # Switch back to starting brand and diff against the temporary
2402 # branch containing the latest rietveld patch.
2403 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2404 finally:
2405 RunGit(['checkout', '-q', branch])
2406 RunGit(['branch', '-D', TMP_BRANCH])
2407
2408 return 0
2409
2410
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002411def CMDowners(parser, args):
2412 """interactively find the owners for reviewing"""
2413 parser.add_option(
2414 '--no-color',
2415 action='store_true',
2416 help='Use this option to disable color output')
2417 options, args = parser.parse_args(args)
2418
2419 author = RunGit(['config', 'user.email']).strip() or None
2420
2421 cl = Changelist()
2422
2423 if args:
2424 if len(args) > 1:
2425 parser.error('Unknown args')
2426 base_branch = args[0]
2427 else:
2428 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002429 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002430
2431 change = cl.GetChange(base_branch, None)
2432 return owners_finder.OwnersFinder(
2433 [f.LocalPath() for f in
2434 cl.GetChange(base_branch, None).AffectedFiles()],
2435 change.RepositoryRoot(), author,
2436 fopen=file, os_path=os.path, glob=glob.glob,
2437 disable_color=options.no_color).run()
2438
2439
enne@chromium.org555cfe42014-01-29 18:21:39 +00002440@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002441def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002442 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002443 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002444 parser.add_option('--full', action='store_true',
2445 help='Reformat the full content of all touched files')
2446 parser.add_option('--dry-run', action='store_true',
2447 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002448 parser.add_option('--diff', action='store_true',
2449 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002450 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002451
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002452 # git diff generates paths against the root of the repository. Change
2453 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002454 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002455 if rel_base_path:
2456 os.chdir(rel_base_path)
2457
digit@chromium.org29e47272013-05-17 17:01:46 +00002458 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002459 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002460 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002461 # Only list the names of modified files.
2462 diff_cmd.append('--name-only')
2463 else:
2464 # Only generate context-less patches.
2465 diff_cmd.append('-U0')
2466
2467 # Grab the merge-base commit, i.e. the upstream commit of the current
2468 # branch when it was created or the last time it was rebased. This is
2469 # to cover the case where the user may have called "git fetch origin",
2470 # moving the origin branch to a newer commit, but hasn't rebased yet.
2471 upstream_commit = None
2472 cl = Changelist()
2473 upstream_branch = cl.GetUpstreamBranch()
2474 if upstream_branch:
2475 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2476 upstream_commit = upstream_commit.strip()
2477
2478 if not upstream_commit:
2479 DieWithError('Could not find base commit for this branch. '
2480 'Are you in detached state?')
2481
2482 diff_cmd.append(upstream_commit)
2483
2484 # Handle source file filtering.
2485 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002486 if args:
2487 for arg in args:
2488 if os.path.isdir(arg):
2489 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2490 elif os.path.isfile(arg):
2491 diff_cmd.append(arg)
2492 else:
2493 DieWithError('Argument "%s" is not a file or a directory' % arg)
2494 else:
2495 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002496 diff_output = RunGit(diff_cmd)
2497
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002498 top_dir = os.path.normpath(
2499 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2500
2501 # Locate the clang-format binary in the checkout
2502 try:
2503 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2504 except clang_format.NotFoundError, e:
2505 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002506
digit@chromium.org29e47272013-05-17 17:01:46 +00002507 if opts.full:
2508 # diff_output is a list of files to send to clang-format.
2509 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002510 if not files:
2511 print "Nothing to format."
2512 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002513 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002514 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002515 cmd.append('-i')
2516 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002517 if opts.diff:
2518 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002519 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002520 env = os.environ.copy()
2521 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002522 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002523 try:
2524 script = clang_format.FindClangFormatScriptInChromiumTree(
2525 'clang-format-diff.py')
2526 except clang_format.NotFoundError, e:
2527 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002528
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002529 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002530 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002531 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002532
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002533 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002534 if opts.diff:
2535 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002536 if opts.dry_run and len(stdout) > 0:
2537 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002538
2539 return 0
2540
2541
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002542class OptionParser(optparse.OptionParser):
2543 """Creates the option parse and add --verbose support."""
2544 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002545 optparse.OptionParser.__init__(
2546 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002547 self.add_option(
2548 '-v', '--verbose', action='count', default=0,
2549 help='Use 2 times for more debugging info')
2550
2551 def parse_args(self, args=None, values=None):
2552 options, args = optparse.OptionParser.parse_args(self, args, values)
2553 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2554 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2555 return options, args
2556
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002557
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002558def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002559 if sys.hexversion < 0x02060000:
2560 print >> sys.stderr, (
2561 '\nYour python version %s is unsupported, please upgrade.\n' %
2562 sys.version.split(' ', 1)[0])
2563 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002564
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002565 # Reload settings.
2566 global settings
2567 settings = Settings()
2568
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002569 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002570 dispatcher = subcommand.CommandDispatcher(__name__)
2571 try:
2572 return dispatcher.execute(OptionParser(), argv)
2573 except urllib2.HTTPError, e:
2574 if e.code != 500:
2575 raise
2576 DieWithError(
2577 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2578 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002579
2580
2581if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002582 # These affect sys.stdout so do it outside of main() to simplify mocks in
2583 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002584 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002585 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002586 sys.exit(main(sys.argv[1:]))