blob: 1acc12a691fa7c21b864069db1178443a223ab84 [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,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000798 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000799
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000800 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000801 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000802
803 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000804 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000805 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000806 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000807 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000808 except presubmit_support.PresubmitFailure, e:
809 DieWithError(
810 ('%s\nMaybe your depot_tools is out of date?\n'
811 'If all fails, contact maruel@') % e)
812
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000813 def UpdateDescription(self, description):
814 self.description = description
815 return self.RpcServer().update_description(
816 self.GetIssue(), self.description)
817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000819 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000820 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000822 def SetFlag(self, flag, value):
823 """Patchset must match."""
824 if not self.GetPatchset():
825 DieWithError('The patchset needs to match. Send another patchset.')
826 try:
827 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000828 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000829 except urllib2.HTTPError, e:
830 if e.code == 404:
831 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
832 if e.code == 403:
833 DieWithError(
834 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
835 'match?') % (self.GetIssue(), self.GetPatchset()))
836 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000838 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839 """Returns an upload.RpcServer() to access this review's rietveld instance.
840 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000841 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000842 self._rpc_server = rietveld.CachingRietveld(
843 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000844 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845
846 def _IssueSetting(self):
847 """Return the git setting that stores this change's issue."""
848 return 'branch.%s.rietveldissue' % self.GetBranch()
849
850 def _PatchsetSetting(self):
851 """Return the git setting that stores this change's most recent patchset."""
852 return 'branch.%s.rietveldpatchset' % self.GetBranch()
853
854 def _RietveldServer(self):
855 """Returns the git setting that stores this change's rietveld server."""
856 return 'branch.%s.rietveldserver' % self.GetBranch()
857
858
859def GetCodereviewSettingsInteractively():
860 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000861 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000862 server = settings.GetDefaultServerUrl(error_ok=True)
863 prompt = 'Rietveld server (host[:port])'
864 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000865 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866 if not server and not newserver:
867 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000868 if newserver:
869 newserver = gclient_utils.UpgradeToHttps(newserver)
870 if newserver != server:
871 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000873 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874 prompt = caption
875 if initial:
876 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000877 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 if new_val == 'x':
879 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000880 elif new_val:
881 if is_url:
882 new_val = gclient_utils.UpgradeToHttps(new_val)
883 if new_val != initial:
884 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000886 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000887 SetProperty(settings.GetDefaultPrivateFlag(),
888 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000890 'tree-status-url', False)
891 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000892 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893
894 # TODO: configure a default branch to diff against, rather than this
895 # svn-based hackery.
896
897
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000898class ChangeDescription(object):
899 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000900 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000901 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000902
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000903 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000904 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000905
agable@chromium.org42c20792013-09-12 17:34:49 +0000906 @property # www.logilab.org/ticket/89786
907 def description(self): # pylint: disable=E0202
908 return '\n'.join(self._description_lines)
909
910 def set_description(self, desc):
911 if isinstance(desc, basestring):
912 lines = desc.splitlines()
913 else:
914 lines = [line.rstrip() for line in desc]
915 while lines and not lines[0]:
916 lines.pop(0)
917 while lines and not lines[-1]:
918 lines.pop(-1)
919 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000920
921 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000922 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000923 assert isinstance(reviewers, list), reviewers
924 if not reviewers:
925 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000926 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000927
agable@chromium.org42c20792013-09-12 17:34:49 +0000928 # Get the set of R= and TBR= lines and remove them from the desciption.
929 regexp = re.compile(self.R_LINE)
930 matches = [regexp.match(line) for line in self._description_lines]
931 new_desc = [l for i, l in enumerate(self._description_lines)
932 if not matches[i]]
933 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000934
agable@chromium.org42c20792013-09-12 17:34:49 +0000935 # Construct new unified R= and TBR= lines.
936 r_names = []
937 tbr_names = []
938 for match in matches:
939 if not match:
940 continue
941 people = cleanup_list([match.group(2).strip()])
942 if match.group(1) == 'TBR':
943 tbr_names.extend(people)
944 else:
945 r_names.extend(people)
946 for name in r_names:
947 if name not in reviewers:
948 reviewers.append(name)
949 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
950 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
951
952 # Put the new lines in the description where the old first R= line was.
953 line_loc = next((i for i, match in enumerate(matches) if match), -1)
954 if 0 <= line_loc < len(self._description_lines):
955 if new_tbr_line:
956 self._description_lines.insert(line_loc, new_tbr_line)
957 if new_r_line:
958 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000959 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000960 if new_r_line:
961 self.append_footer(new_r_line)
962 if new_tbr_line:
963 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000964
965 def prompt(self):
966 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000967 self.set_description([
968 '# Enter a description of the change.',
969 '# This will be displayed on the codereview site.',
970 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000971 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000972 '--------------------',
973 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000974
agable@chromium.org42c20792013-09-12 17:34:49 +0000975 regexp = re.compile(self.BUG_LINE)
976 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000977 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000978 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000979 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000980 if not content:
981 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000982 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000983
984 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000985 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
986 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000987 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000988 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000989
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000990 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000991 if self._description_lines:
992 # Add an empty line if either the last line or the new line isn't a tag.
993 last_line = self._description_lines[-1]
994 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
995 not presubmit_support.Change.TAG_LINE_RE.match(line)):
996 self._description_lines.append('')
997 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000998
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000999 def get_reviewers(self):
1000 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001001 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1002 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001003 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001004
1005
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001006def get_approving_reviewers(props):
1007 """Retrieves the reviewers that approved a CL from the issue properties with
1008 messages.
1009
1010 Note that the list may contain reviewers that are not committer, thus are not
1011 considered by the CQ.
1012 """
1013 return sorted(
1014 set(
1015 message['sender']
1016 for message in props['messages']
1017 if message['approval'] and message['sender'] in props['reviewers']
1018 )
1019 )
1020
1021
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022def FindCodereviewSettingsFile(filename='codereview.settings'):
1023 """Finds the given file starting in the cwd and going up.
1024
1025 Only looks up to the top of the repository unless an
1026 'inherit-review-settings-ok' file exists in the root of the repository.
1027 """
1028 inherit_ok_file = 'inherit-review-settings-ok'
1029 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001030 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1032 root = '/'
1033 while True:
1034 if filename in os.listdir(cwd):
1035 if os.path.isfile(os.path.join(cwd, filename)):
1036 return open(os.path.join(cwd, filename))
1037 if cwd == root:
1038 break
1039 cwd = os.path.dirname(cwd)
1040
1041
1042def LoadCodereviewSettingsFromFile(fileobj):
1043 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001044 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 def SetProperty(name, setting, unset_error_ok=False):
1047 fullname = 'rietveld.' + name
1048 if setting in keyvals:
1049 RunGit(['config', fullname, keyvals[setting]])
1050 else:
1051 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1052
1053 SetProperty('server', 'CODE_REVIEW_SERVER')
1054 # Only server setting is required. Other settings can be absent.
1055 # In that case, we ignore errors raised during option deletion attempt.
1056 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001057 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1059 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001060 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001061 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1062 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001064 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001065 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001066
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1068 #should be of the form
1069 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1070 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1071 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1072 keyvals['ORIGIN_URL_CONFIG']])
1073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001075def urlretrieve(source, destination):
1076 """urllib is broken for SSL connections via a proxy therefore we
1077 can't use urllib.urlretrieve()."""
1078 with open(destination, 'w') as f:
1079 f.write(urllib2.urlopen(source).read())
1080
1081
ukai@chromium.org712d6102013-11-27 00:52:58 +00001082def hasSheBang(fname):
1083 """Checks fname is a #! script."""
1084 with open(fname) as f:
1085 return f.read(2).startswith('#!')
1086
1087
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001088def DownloadHooks(force):
1089 """downloads hooks
1090
1091 Args:
1092 force: True to update hooks. False to install hooks if not present.
1093 """
1094 if not settings.GetIsGerrit():
1095 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001096 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001097 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1098 if not os.access(dst, os.X_OK):
1099 if os.path.exists(dst):
1100 if not force:
1101 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001102 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001103 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001104 if not hasSheBang(dst):
1105 DieWithError('Not a script: %s\n'
1106 'You need to download from\n%s\n'
1107 'into .git/hooks/commit-msg and '
1108 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001109 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1110 except Exception:
1111 if os.path.exists(dst):
1112 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001113 DieWithError('\nFailed to download hooks.\n'
1114 'You need to download from\n%s\n'
1115 'into .git/hooks/commit-msg and '
1116 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001117
1118
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001119@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001121 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001123 parser.add_option('--activate-update', action='store_true',
1124 help='activate auto-updating [rietveld] section in '
1125 '.git/config')
1126 parser.add_option('--deactivate-update', action='store_true',
1127 help='deactivate auto-updating [rietveld] section in '
1128 '.git/config')
1129 options, args = parser.parse_args(args)
1130
1131 if options.deactivate_update:
1132 RunGit(['config', 'rietveld.autoupdate', 'false'])
1133 return
1134
1135 if options.activate_update:
1136 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1137 return
1138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 if len(args) == 0:
1140 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001141 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 return 0
1143
1144 url = args[0]
1145 if not url.endswith('codereview.settings'):
1146 url = os.path.join(url, 'codereview.settings')
1147
1148 # Load code review settings and download hooks (if available).
1149 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001150 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 return 0
1152
1153
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001154def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001155 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001156 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1157 branch = ShortBranchName(branchref)
1158 _, args = parser.parse_args(args)
1159 if not args:
1160 print("Current base-url:")
1161 return RunGit(['config', 'branch.%s.base-url' % branch],
1162 error_ok=False).strip()
1163 else:
1164 print("Setting base-url to %s" % args[0])
1165 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1166 error_ok=False).strip()
1167
1168
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001170 """Show status of changelists.
1171
1172 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001173 - Red not sent for review or broken
1174 - Blue waiting for review
1175 - Yellow waiting for you to reply to review
1176 - Green LGTM'ed
1177 - Magenta in the commit queue
1178 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001179
1180 Also see 'git cl comments'.
1181 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 parser.add_option('--field',
1183 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001184 parser.add_option('-f', '--fast', action='store_true',
1185 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001187 if args:
1188 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001191 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 if options.field.startswith('desc'):
1193 print cl.GetDescription()
1194 elif options.field == 'id':
1195 issueid = cl.GetIssue()
1196 if issueid:
1197 print issueid
1198 elif options.field == 'patch':
1199 patchset = cl.GetPatchset()
1200 if patchset:
1201 print patchset
1202 elif options.field == 'url':
1203 url = cl.GetIssueURL()
1204 if url:
1205 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001206 return 0
1207
1208 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1209 if not branches:
1210 print('No local branch found.')
1211 return 0
1212
1213 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001214 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001215 alignment = max(5, max(len(b) for b in branches))
1216 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 # Adhoc thread pool to request data concurrently.
1218 output = Queue.Queue()
1219
1220 # Silence upload.py otherwise it becomes unweldly.
1221 upload.verbosity = 0
1222
1223 if not options.fast:
1224 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001225 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001226 c = Changelist(branchref=b)
1227 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001228 props = {}
1229 r = None
1230 if i:
1231 try:
1232 props = c.GetIssueProperties()
1233 r = c.GetApprovingReviewers() if i else None
1234 except urllib2.HTTPError:
1235 # The issue probably doesn't exist anymore.
1236 i += ' (broken)'
1237
1238 msgs = props.get('messages') or []
1239
1240 if not i:
1241 color = Fore.WHITE
1242 elif props.get('closed'):
1243 # Issue is closed.
1244 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001245 elif props.get('commit'):
1246 # Issue is in the commit queue.
1247 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001248 elif r:
1249 # Was LGTM'ed.
1250 color = Fore.GREEN
1251 elif not msgs:
1252 # No message was sent.
1253 color = Fore.RED
1254 elif msgs[-1]['sender'] != props.get('owner_email'):
1255 color = Fore.YELLOW
1256 else:
1257 color = Fore.BLUE
1258 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001259
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001260 # Process one branch synchronously to work through authentication, then
1261 # spawn threads to process all the other branches in parallel.
1262 if branches:
1263 fetch(branches[0])
1264 threads = [
1265 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001266 for t in threads:
1267 t.daemon = True
1268 t.start()
1269 else:
1270 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1271 for b in branches:
1272 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001273 url = c.GetIssueURL()
1274 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001275
1276 tmp = {}
1277 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001278 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001279 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001280 b, i, color = output.get()
1281 tmp[b] = (i, color)
1282 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001283 reset = Fore.RESET
1284 if not sys.stdout.isatty():
1285 color = ''
1286 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001287 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001288 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001289
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001290 cl = Changelist()
1291 print
1292 print 'Current branch:',
1293 if not cl.GetIssue():
1294 print 'no issue assigned.'
1295 return 0
1296 print cl.GetBranch()
1297 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1298 print 'Issue description:'
1299 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 return 0
1301
1302
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001303def colorize_CMDstatus_doc():
1304 """To be called once in main() to add colors to git cl status help."""
1305 colors = [i for i in dir(Fore) if i[0].isupper()]
1306
1307 def colorize_line(line):
1308 for color in colors:
1309 if color in line.upper():
1310 # Extract whitespaces first and the leading '-'.
1311 indent = len(line) - len(line.lstrip(' ')) + 1
1312 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1313 return line
1314
1315 lines = CMDstatus.__doc__.splitlines()
1316 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1317
1318
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001319@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001321 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001324 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001325 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326
1327 cl = Changelist()
1328 if len(args) > 0:
1329 try:
1330 issue = int(args[0])
1331 except ValueError:
1332 DieWithError('Pass a number to set the issue or none to list it.\n'
1333 'Maybe you want to run git cl status?')
1334 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001335 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336 return 0
1337
1338
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001339def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001340 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001341 (_, args) = parser.parse_args(args)
1342 if args:
1343 parser.error('Unsupported argument: %s' % args)
1344
1345 cl = Changelist()
1346 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001347 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001348 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001349 if message['disapproval']:
1350 color = Fore.RED
1351 elif message['approval']:
1352 color = Fore.GREEN
1353 elif message['sender'] == data['owner_email']:
1354 color = Fore.MAGENTA
1355 else:
1356 color = Fore.BLUE
1357 print '\n%s%s %s%s' % (
1358 color, message['date'].split('.', 1)[0], message['sender'],
1359 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001360 if message['text'].strip():
1361 print '\n'.join(' ' + l for l in message['text'].splitlines())
1362 return 0
1363
1364
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001365def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001366 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001367 cl = Changelist()
1368 if not cl.GetIssue():
1369 DieWithError('This branch has no associated changelist.')
1370 description = ChangeDescription(cl.GetDescription())
1371 description.prompt()
1372 cl.UpdateDescription(description.description)
1373 return 0
1374
1375
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376def CreateDescriptionFromLog(args):
1377 """Pulls out the commit log to use as a base for the CL description."""
1378 log_args = []
1379 if len(args) == 1 and not args[0].endswith('.'):
1380 log_args = [args[0] + '..']
1381 elif len(args) == 1 and args[0].endswith('...'):
1382 log_args = [args[0][:-1]]
1383 elif len(args) == 2:
1384 log_args = [args[0] + '..' + args[1]]
1385 else:
1386 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001387 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388
1389
thestig@chromium.org44202a22014-03-11 19:22:18 +00001390def CMDlint(parser, args):
1391 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001392 parser.add_option('--filter', action='append', metavar='-x,+y',
1393 help='Comma-separated list of cpplint\'s category-filters')
1394 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001395
1396 # Access to a protected member _XX of a client class
1397 # pylint: disable=W0212
1398 try:
1399 import cpplint
1400 import cpplint_chromium
1401 except ImportError:
1402 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1403 return 1
1404
1405 # Change the current working directory before calling lint so that it
1406 # shows the correct base.
1407 previous_cwd = os.getcwd()
1408 os.chdir(settings.GetRoot())
1409 try:
1410 cl = Changelist()
1411 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1412 files = [f.LocalPath() for f in change.AffectedFiles()]
1413
1414 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001415 command = args + files
1416 if options.filter:
1417 command = ['--filter=' + ','.join(options.filter)] + command
1418 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001419
1420 white_regex = re.compile(settings.GetLintRegex())
1421 black_regex = re.compile(settings.GetLintIgnoreRegex())
1422 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1423 for filename in filenames:
1424 if white_regex.match(filename):
1425 if black_regex.match(filename):
1426 print "Ignoring file %s" % filename
1427 else:
1428 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1429 extra_check_functions)
1430 else:
1431 print "Skipping file %s" % filename
1432 finally:
1433 os.chdir(previous_cwd)
1434 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1435 if cpplint._cpplint_state.error_count != 0:
1436 return 1
1437 return 0
1438
1439
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001441 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001442 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001444 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001445 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 (options, args) = parser.parse_args(args)
1447
ukai@chromium.org259e4682012-10-25 07:36:33 +00001448 if not options.force and is_dirty_git_tree('presubmit'):
1449 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 return 1
1451
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001452 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 if args:
1454 base_branch = args[0]
1455 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001456 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001457 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001459 cl.RunHook(
1460 committing=not options.upload,
1461 may_prompt=False,
1462 verbose=options.verbose,
1463 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001464 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465
1466
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001467def AddChangeIdToCommitMessage(options, args):
1468 """Re-commits using the current message, assumes the commit hook is in
1469 place.
1470 """
1471 log_desc = options.message or CreateDescriptionFromLog(args)
1472 git_command = ['commit', '--amend', '-m', log_desc]
1473 RunGit(git_command)
1474 new_log_desc = CreateDescriptionFromLog(args)
1475 if CHANGE_ID in new_log_desc:
1476 print 'git-cl: Added Change-Id to commit message.'
1477 else:
1478 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1479
1480
ukai@chromium.orge8077812012-02-03 03:41:46 +00001481def GerritUpload(options, args, cl):
1482 """upload the current branch to gerrit."""
1483 # We assume the remote called "origin" is the one we want.
1484 # It is probably not worthwhile to support different workflows.
1485 remote = 'origin'
1486 branch = 'master'
1487 if options.target_branch:
1488 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001490 change_desc = ChangeDescription(
1491 options.message or CreateDescriptionFromLog(args))
1492 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001493 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001494 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001495 if CHANGE_ID not in change_desc.description:
1496 AddChangeIdToCommitMessage(options, args)
1497 if options.reviewers:
1498 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001499
ukai@chromium.orge8077812012-02-03 03:41:46 +00001500 receive_options = []
1501 cc = cl.GetCCList().split(',')
1502 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001503 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001504 cc = filter(None, cc)
1505 if cc:
1506 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001507 if change_desc.get_reviewers():
1508 receive_options.extend(
1509 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510
ukai@chromium.orge8077812012-02-03 03:41:46 +00001511 git_command = ['push']
1512 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001513 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001514 ' '.join(receive_options))
1515 git_command += [remote, 'HEAD:refs/for/' + branch]
1516 RunGit(git_command)
1517 # TODO(ukai): parse Change-Id: and set issue number?
1518 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520
ukai@chromium.orge8077812012-02-03 03:41:46 +00001521def RietveldUpload(options, args, cl):
1522 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001523 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1524 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001525 if options.emulate_svn_auto_props:
1526 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527
1528 change_desc = None
1529
pgervais@chromium.org91141372014-01-09 23:27:20 +00001530 if options.email is not None:
1531 upload_args.extend(['--email', options.email])
1532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001534 if options.title:
1535 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001536 if options.message:
1537 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001538 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001539 print ("This branch is associated with issue %s. "
1540 "Adding patch to that issue." % cl.GetIssue())
1541 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001542 if options.title:
1543 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001544 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001545 change_desc = ChangeDescription(message)
1546 if options.reviewers:
1547 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001548 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001549 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001550
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001551 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 print "Description is empty; aborting."
1553 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001554
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001555 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001556 if change_desc.get_reviewers():
1557 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001558 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001559 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001560 DieWithError("Must specify reviewers to send email.")
1561 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001562
1563 # We check this before applying rietveld.private assuming that in
1564 # rietveld.cc only addresses which we can send private CLs to are listed
1565 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1566 # --private is specified explicitly on the command line.
1567 if options.private:
1568 logging.warn('rietveld.cc is ignored since private flag is specified. '
1569 'You need to review and add them manually if necessary.')
1570 cc = cl.GetCCListWithoutDefault()
1571 else:
1572 cc = cl.GetCCList()
1573 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001574 if cc:
1575 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001577 if options.private or settings.GetDefaultPrivateFlag() == "True":
1578 upload_args.append('--private')
1579
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001580 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001581 if not options.find_copies:
1582 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001583
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584 # Include the upstream repo's URL in the change -- this is useful for
1585 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001586 remote_url = cl.GetGitBaseUrlFromConfig()
1587 if not remote_url:
1588 if settings.GetIsGitSvn():
1589 # URL is dependent on the current directory.
1590 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1591 if data:
1592 keys = dict(line.split(': ', 1) for line in data.splitlines()
1593 if ': ' in line)
1594 remote_url = keys.get('URL', None)
1595 else:
1596 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1597 remote_url = (cl.GetRemoteUrl() + '@'
1598 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001599 if remote_url:
1600 upload_args.extend(['--base_url', remote_url])
1601
1602 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001603 upload_args = ['upload'] + upload_args + args
1604 logging.info('upload.RealMain(%s)', upload_args)
1605 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001606 issue = int(issue)
1607 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001608 except KeyboardInterrupt:
1609 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610 except:
1611 # If we got an exception after the user typed a description for their
1612 # change, back up the description before re-raising.
1613 if change_desc:
1614 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1615 print '\nGot exception while uploading -- saving description to %s\n' \
1616 % backup_path
1617 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001618 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619 backup_file.close()
1620 raise
1621
1622 if not cl.GetIssue():
1623 cl.SetIssue(issue)
1624 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001625
1626 if options.use_commit_queue:
1627 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628 return 0
1629
1630
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001631def cleanup_list(l):
1632 """Fixes a list so that comma separated items are put as individual items.
1633
1634 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1635 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1636 """
1637 items = sum((i.split(',') for i in l), [])
1638 stripped_items = (i.strip() for i in items)
1639 return sorted(filter(None, stripped_items))
1640
1641
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001642@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001643def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001644 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001645 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1646 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001647 parser.add_option('--bypass-watchlists', action='store_true',
1648 dest='bypass_watchlists',
1649 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001650 parser.add_option('-f', action='store_true', dest='force',
1651 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001652 parser.add_option('-m', dest='message', help='message for patchset')
1653 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001654 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001655 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001656 help='reviewer email addresses')
1657 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001658 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001659 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001660 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001661 help='send email to reviewer immediately')
1662 parser.add_option("--emulate_svn_auto_props", action="store_true",
1663 dest="emulate_svn_auto_props",
1664 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001665 parser.add_option('-c', '--use-commit-queue', action='store_true',
1666 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001667 parser.add_option('--private', action='store_true',
1668 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001669 parser.add_option('--target_branch',
1670 help='When uploading to gerrit, remote branch to '
1671 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001672 parser.add_option('--email', default=None,
1673 help='email address to use to connect to Rietveld')
1674
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001675 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001676 (options, args) = parser.parse_args(args)
1677
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001678 if options.target_branch and not settings.GetIsGerrit():
1679 parser.error('Use --target_branch for non gerrit repository.')
1680
ukai@chromium.org259e4682012-10-25 07:36:33 +00001681 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001682 return 1
1683
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001684 options.reviewers = cleanup_list(options.reviewers)
1685 options.cc = cleanup_list(options.cc)
1686
ukai@chromium.orge8077812012-02-03 03:41:46 +00001687 cl = Changelist()
1688 if args:
1689 # TODO(ukai): is it ok for gerrit case?
1690 base_branch = args[0]
1691 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001692 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001693 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001694 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001695
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001696 # Apply watchlists on upload.
1697 change = cl.GetChange(base_branch, None)
1698 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1699 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001700 if not options.bypass_watchlists:
1701 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001702
ukai@chromium.orge8077812012-02-03 03:41:46 +00001703 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001704 if options.reviewers:
1705 # Set the reviewer list now so that presubmit checks can access it.
1706 change_description = ChangeDescription(change.FullDescriptionText())
1707 change_description.update_reviewers(options.reviewers)
1708 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001709 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001710 may_prompt=not options.force,
1711 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001712 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001713 if not hook_results.should_continue():
1714 return 1
1715 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001716 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001717
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001718 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001719 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001720 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001721 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001722 print ('The last upload made from this repository was patchset #%d but '
1723 'the most recent patchset on the server is #%d.'
1724 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001725 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1726 'from another machine or branch the patch you\'re uploading now '
1727 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001728 ask_for_data('About to upload; enter to confirm.')
1729
iannucci@chromium.org79540052012-10-19 23:15:26 +00001730 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001731 if settings.GetIsGerrit():
1732 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001733 ret = RietveldUpload(options, args, cl)
1734 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001735 git_set_branch_value('last-upload-hash',
1736 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001737
1738 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001739
1740
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001741def IsSubmoduleMergeCommit(ref):
1742 # When submodules are added to the repo, we expect there to be a single
1743 # non-git-svn merge commit at remote HEAD with a signature comment.
1744 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001745 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001746 return RunGit(cmd) != ''
1747
1748
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749def SendUpstream(parser, args, cmd):
1750 """Common code for CmdPush and CmdDCommit
1751
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001752 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753 Updates changelog with metadata (e.g. pointer to review).
1754 Pushes/dcommits the code upstream.
1755 Updates review and closes.
1756 """
1757 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1758 help='bypass upload presubmit hook')
1759 parser.add_option('-m', dest='message',
1760 help="override review description")
1761 parser.add_option('-f', action='store_true', dest='force',
1762 help="force yes to questions (don't prompt)")
1763 parser.add_option('-c', dest='contributor',
1764 help="external contributor for patch (appended to " +
1765 "description and used as author for git). Should be " +
1766 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001767 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 (options, args) = parser.parse_args(args)
1769 cl = Changelist()
1770
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001771 current = cl.GetBranch()
1772 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1773 if not settings.GetIsGitSvn() and remote == '.':
1774 print
1775 print 'Attempting to push branch %r into another local branch!' % current
1776 print
1777 print 'Either reparent this branch on top of origin/master:'
1778 print ' git reparent-branch --root'
1779 print
1780 print 'OR run `git rebase-update` if you think the parent branch is already'
1781 print 'committed.'
1782 print
1783 print ' Current parent: %r' % upstream_branch
1784 return 1
1785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786 if not args or cmd == 'push':
1787 # Default to merging against our best guess of the upstream branch.
1788 args = [cl.GetUpstreamBranch()]
1789
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001790 if options.contributor:
1791 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1792 print "Please provide contibutor as 'First Last <email@example.com>'"
1793 return 1
1794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001796 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797
ukai@chromium.org259e4682012-10-25 07:36:33 +00001798 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001799 return 1
1800
1801 # This rev-list syntax means "show all commits not in my branch that
1802 # are in base_branch".
1803 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1804 base_branch]).splitlines()
1805 if upstream_commits:
1806 print ('Base branch "%s" has %d commits '
1807 'not in this branch.' % (base_branch, len(upstream_commits)))
1808 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1809 return 1
1810
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001811 # This is the revision `svn dcommit` will commit on top of.
1812 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1813 '--pretty=format:%H'])
1814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001816 # If the base_head is a submodule merge commit, the first parent of the
1817 # base_head should be a git-svn commit, which is what we're interested in.
1818 base_svn_head = base_branch
1819 if base_has_submodules:
1820 base_svn_head += '^1'
1821
1822 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001823 if extra_commits:
1824 print ('This branch has %d additional commits not upstreamed yet.'
1825 % len(extra_commits.splitlines()))
1826 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1827 'before attempting to %s.' % (base_branch, cmd))
1828 return 1
1829
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001830 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001831 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001832 author = None
1833 if options.contributor:
1834 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001835 hook_results = cl.RunHook(
1836 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001837 may_prompt=not options.force,
1838 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001839 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001840 if not hook_results.should_continue():
1841 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842
1843 if cmd == 'dcommit':
1844 # Check the tree status if the tree status URL is set.
1845 status = GetTreeStatus()
1846 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001847 print('The tree is closed. Please wait for it to reopen. Use '
1848 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 return 1
1850 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001851 print('Unable to determine tree status. Please verify manually and '
1852 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001853 else:
1854 breakpad.SendStack(
1855 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001856 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1857 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001858 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001860 change_desc = ChangeDescription(options.message)
1861 if not change_desc.description and cl.GetIssue():
1862 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001864 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001865 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001866 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001867 else:
1868 print 'No description set.'
1869 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1870 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001872 # Keep a separate copy for the commit message, because the commit message
1873 # contains the link to the Rietveld issue, while the Rietveld message contains
1874 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001875 # Keep a separate copy for the commit message.
1876 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001877 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001878
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001879 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001880 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001881 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001882 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001883 commit_desc.append_footer('Patch from %s.' % options.contributor)
1884
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001885 print('Description:')
1886 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887
1888 branches = [base_branch, cl.GetBranchRef()]
1889 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001890 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001891 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001893 # We want to squash all this branch's commits into one commit with the proper
1894 # description. We do this by doing a "reset --soft" to the base branch (which
1895 # keeps the working copy the same), then dcommitting that. If origin/master
1896 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1897 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001899 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1900 # Delete the branches if they exist.
1901 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1902 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1903 result = RunGitWithCode(showref_cmd)
1904 if result[0] == 0:
1905 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906
1907 # We might be in a directory that's present in this branch but not in the
1908 # trunk. Move up to the top of the tree so that git commands that expect a
1909 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001910 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911 if rel_base_path:
1912 os.chdir(rel_base_path)
1913
1914 # Stuff our change into the merge branch.
1915 # We wrap in a try...finally block so if anything goes wrong,
1916 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001917 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001919 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1920 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001922 RunGit(
1923 [
1924 'commit', '--author', options.contributor,
1925 '-m', commit_desc.description,
1926 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001928 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001929 if base_has_submodules:
1930 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1931 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1932 RunGit(['checkout', CHERRY_PICK_BRANCH])
1933 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934 if cmd == 'push':
1935 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001936 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001937 retcode, output = RunGitWithCode(
1938 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1939 logging.debug(output)
1940 else:
1941 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001942 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001943 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001944 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945 finally:
1946 # And then swap back to the original branch and clean up.
1947 RunGit(['checkout', '-q', cl.GetBranch()])
1948 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001949 if base_has_submodules:
1950 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951
1952 if cl.GetIssue():
1953 if cmd == 'dcommit' and 'Committed r' in output:
1954 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1955 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001956 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1957 for l in output.splitlines(False))
1958 match = filter(None, match)
1959 if len(match) != 1:
1960 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1961 output)
1962 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 else:
1964 return 1
1965 viewvc_url = settings.GetViewVCUrl()
1966 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001967 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001968 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001969 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970 print ('Closing issue '
1971 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001972 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001974 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001975 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001976 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001977 if options.bypass_hooks:
1978 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1979 else:
1980 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001981 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001982 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001983
1984 if retcode == 0:
1985 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1986 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001987 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001988
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989 return 0
1990
1991
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001992@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001994 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001996 message = """This doesn't appear to be an SVN repository.
1997If your project has a git mirror with an upstream SVN master, you probably need
1998to run 'git svn init', see your project's git mirror documentation.
1999If your project has a true writeable upstream repository, you probably want
2000to run 'git cl push' instead.
2001Choose wisely, if you get this wrong, your commit might appear to succeed but
2002will instead be silently ignored."""
2003 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002004 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002005 return SendUpstream(parser, args, 'dcommit')
2006
2007
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002008@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002010 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 if settings.GetIsGitSvn():
2012 print('This appears to be an SVN repository.')
2013 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002014 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015 return SendUpstream(parser, args, 'push')
2016
2017
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002018@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002020 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002021 parser.add_option('-b', dest='newbranch',
2022 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002023 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002025 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2026 help='Change to the directory DIR immediately, '
2027 'before doing anything else.')
2028 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002029 help='failed patches spew .rej files rather than '
2030 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002031 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2032 help="don't commit after patch applies")
2033 (options, args) = parser.parse_args(args)
2034 if len(args) != 1:
2035 parser.print_help()
2036 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002037 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002039 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002040 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002041
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002042 if options.newbranch:
2043 if options.force:
2044 RunGit(['branch', '-D', options.newbranch],
2045 stderr=subprocess2.PIPE, error_ok=True)
2046 RunGit(['checkout', '-b', options.newbranch,
2047 Changelist().GetUpstreamBranch()])
2048
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002049 return PatchIssue(issue_arg, options.reject, options.nocommit,
2050 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002051
2052
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002053def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002054 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002056 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002057 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002058 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002059 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002061 # Assume it's a URL to the patch. Default to https.
2062 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002063 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002064 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 DieWithError('Must pass an issue ID or full URL for '
2066 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002067 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002068 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002069 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071 # Switch up to the top-level directory, if necessary, in preparation for
2072 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002073 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002074 if top:
2075 os.chdir(top)
2076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077 # Git patches have a/ at the beginning of source paths. We strip that out
2078 # with a sed script rather than the -p flag to patch so we can feed either
2079 # Git or svn-style patches into the same apply command.
2080 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002081 try:
2082 patch_data = subprocess2.check_output(
2083 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2084 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002085 DieWithError('Git patch mungling failed.')
2086 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002088 # We use "git apply" to apply the patch instead of "patch" so that we can
2089 # pick up file adds.
2090 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002091 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002092 if directory:
2093 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002094 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002095 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002096 elif IsGitVersionAtLeast('1.7.12'):
2097 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002098 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002099 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002100 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002101 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102 DieWithError('Failed to apply the patch')
2103
2104 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002105 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2107 cl = Changelist()
2108 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002109 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002110 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 else:
2112 print "Patch applied to index."
2113 return 0
2114
2115
2116def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002117 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 # Provide a wrapper for git svn rebase to help avoid accidental
2119 # git svn dcommit.
2120 # It's the only command that doesn't use parser at all since we just defer
2121 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002122
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002123 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
2125
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002126def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 """Fetches the tree status and returns either 'open', 'closed',
2128 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002129 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130 if url:
2131 status = urllib2.urlopen(url).read().lower()
2132 if status.find('closed') != -1 or status == '0':
2133 return 'closed'
2134 elif status.find('open') != -1 or status == '1':
2135 return 'open'
2136 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002137 return 'unset'
2138
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002140def GetTreeStatusReason():
2141 """Fetches the tree status from a json url and returns the message
2142 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002143 url = settings.GetTreeStatusUrl()
2144 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002145 connection = urllib2.urlopen(json_url)
2146 status = json.loads(connection.read())
2147 connection.close()
2148 return status['message']
2149
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002150
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002152 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002153 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154 status = GetTreeStatus()
2155 if 'unset' == status:
2156 print 'You must configure your tree status URL by running "git cl config".'
2157 return 2
2158
2159 print "The tree is %s" % status
2160 print
2161 print GetTreeStatusReason()
2162 if status != 'open':
2163 return 1
2164 return 0
2165
2166
maruel@chromium.org15192402012-09-06 12:38:29 +00002167def CMDtry(parser, args):
2168 """Triggers a try job through Rietveld."""
2169 group = optparse.OptionGroup(parser, "Try job options")
2170 group.add_option(
2171 "-b", "--bot", action="append",
2172 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2173 "times to specify multiple builders. ex: "
2174 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2175 "the try server waterfall for the builders name and the tests "
2176 "available. Can also be used to specify gtest_filter, e.g. "
2177 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2178 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002179 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002180 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002181 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002182 "-r", "--revision",
2183 help="Revision to use for the try job; default: the "
2184 "revision will be determined by the try server; see "
2185 "its waterfall for more info")
2186 group.add_option(
2187 "-c", "--clobber", action="store_true", default=False,
2188 help="Force a clobber before building; e.g. don't do an "
2189 "incremental build")
2190 group.add_option(
2191 "--project",
2192 help="Override which project to use. Projects are defined "
2193 "server-side to define what default bot set to use")
2194 group.add_option(
2195 "-t", "--testfilter", action="append", default=[],
2196 help=("Apply a testfilter to all the selected builders. Unless the "
2197 "builders configurations are similar, use multiple "
2198 "--bot <builder>:<test> arguments."))
2199 group.add_option(
2200 "-n", "--name", help="Try job name; default to current branch name")
2201 parser.add_option_group(group)
2202 options, args = parser.parse_args(args)
2203
2204 if args:
2205 parser.error('Unknown arguments: %s' % args)
2206
2207 cl = Changelist()
2208 if not cl.GetIssue():
2209 parser.error('Need to upload first')
2210
2211 if not options.name:
2212 options.name = cl.GetBranch()
2213
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002214 if options.bot and not options.master:
2215 parser.error('For manually specified bots please also specify the '
2216 'tryserver master, e.g. "-m tryserver.chromium".')
2217
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002218 def GetMasterMap():
2219 # Process --bot and --testfilter.
2220 if not options.bot:
2221 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002222
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002223 # Get try masters from PRESUBMIT.py files.
2224 masters = presubmit_support.DoGetTryMasters(
2225 change,
2226 change.LocalPaths(),
2227 settings.GetRoot(),
2228 None,
2229 None,
2230 options.verbose,
2231 sys.stdout)
2232 if masters:
2233 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002234
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002235 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2236 options.bot = presubmit_support.DoGetTrySlaves(
2237 change,
2238 change.LocalPaths(),
2239 settings.GetRoot(),
2240 None,
2241 None,
2242 options.verbose,
2243 sys.stdout)
2244 if not options.bot:
2245 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002246
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002247 builders_and_tests = {}
2248 # TODO(machenbach): The old style command-line options don't support
2249 # multiple try masters yet.
2250 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2251 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2252
2253 for bot in old_style:
2254 if ':' in bot:
2255 builder, tests = bot.split(':', 1)
2256 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2257 elif ',' in bot:
2258 parser.error('Specify one bot per --bot flag')
2259 else:
2260 builders_and_tests.setdefault(bot, []).append('defaulttests')
2261
2262 for bot, tests in new_style:
2263 builders_and_tests.setdefault(bot, []).extend(tests)
2264
2265 # Return a master map with one master to be backwards compatible. The
2266 # master name defaults to an empty string, which will cause the master
2267 # not to be set on rietveld (deprecated).
2268 return {options.master: builders_and_tests}
2269
2270 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002271
maruel@chromium.org15192402012-09-06 12:38:29 +00002272 if options.testfilter:
2273 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002274 masters = dict((master, dict(
2275 (b, forced_tests) for b, t in slaves.iteritems()
2276 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002277
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002278 for builders in masters.itervalues():
2279 if any('triggered' in b for b in builders):
2280 print >> sys.stderr, (
2281 'ERROR You are trying to send a job to a triggered bot. This type of'
2282 ' bot requires an\ninitial job from a parent (usually a builder). '
2283 'Instead send your job to the parent.\n'
2284 'Bot list: %s' % builders)
2285 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002286
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002287 patchset = cl.GetMostRecentPatchset()
2288 if patchset and patchset != cl.GetPatchset():
2289 print(
2290 '\nWARNING Mismatch between local config and server. Did a previous '
2291 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2292 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002293 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002294 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002295 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002296 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002297 except urllib2.HTTPError, e:
2298 if e.code == 404:
2299 print('404 from rietveld; '
2300 'did you mean to use "git try" instead of "git cl try"?')
2301 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002302 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002303
2304 for (master, builders) in masters.iteritems():
2305 if master:
2306 print 'Master: %s' % master
2307 length = max(len(builder) for builder in builders)
2308 for builder in sorted(builders):
2309 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002310 return 0
2311
2312
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002313@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002314def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002315 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002316 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002317 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002318 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002319
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002320 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002321 if args:
2322 # One arg means set upstream branch.
2323 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2324 cl = Changelist()
2325 print "Upstream branch set to " + cl.GetUpstreamBranch()
2326 else:
2327 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002328 return 0
2329
2330
thestig@chromium.org00858c82013-12-02 23:08:03 +00002331def CMDweb(parser, args):
2332 """Opens the current CL in the web browser."""
2333 _, args = parser.parse_args(args)
2334 if args:
2335 parser.error('Unrecognized args: %s' % ' '.join(args))
2336
2337 issue_url = Changelist().GetIssueURL()
2338 if not issue_url:
2339 print >> sys.stderr, 'ERROR No issue to open'
2340 return 1
2341
2342 webbrowser.open(issue_url)
2343 return 0
2344
2345
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002346def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002347 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002348 _, args = parser.parse_args(args)
2349 if args:
2350 parser.error('Unrecognized args: %s' % ' '.join(args))
2351 cl = Changelist()
2352 cl.SetFlag('commit', '1')
2353 return 0
2354
2355
groby@chromium.org411034a2013-02-26 15:12:01 +00002356def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002357 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002358 _, args = parser.parse_args(args)
2359 if args:
2360 parser.error('Unrecognized args: %s' % ' '.join(args))
2361 cl = Changelist()
2362 # Ensure there actually is an issue to close.
2363 cl.GetDescription()
2364 cl.CloseIssue()
2365 return 0
2366
2367
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002368def CMDdiff(parser, args):
2369 """shows differences between local tree and last upload."""
2370 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002371 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002372 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002373 if not issue:
2374 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002375 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002376 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002377
2378 # Create a new branch based on the merge-base
2379 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2380 try:
2381 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002382 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002383 if rtn != 0:
2384 return rtn
2385
2386 # Switch back to starting brand and diff against the temporary
2387 # branch containing the latest rietveld patch.
2388 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2389 finally:
2390 RunGit(['checkout', '-q', branch])
2391 RunGit(['branch', '-D', TMP_BRANCH])
2392
2393 return 0
2394
2395
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002396def CMDowners(parser, args):
2397 """interactively find the owners for reviewing"""
2398 parser.add_option(
2399 '--no-color',
2400 action='store_true',
2401 help='Use this option to disable color output')
2402 options, args = parser.parse_args(args)
2403
2404 author = RunGit(['config', 'user.email']).strip() or None
2405
2406 cl = Changelist()
2407
2408 if args:
2409 if len(args) > 1:
2410 parser.error('Unknown args')
2411 base_branch = args[0]
2412 else:
2413 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002414 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002415
2416 change = cl.GetChange(base_branch, None)
2417 return owners_finder.OwnersFinder(
2418 [f.LocalPath() for f in
2419 cl.GetChange(base_branch, None).AffectedFiles()],
2420 change.RepositoryRoot(), author,
2421 fopen=file, os_path=os.path, glob=glob.glob,
2422 disable_color=options.no_color).run()
2423
2424
enne@chromium.org555cfe42014-01-29 18:21:39 +00002425@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002426def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002427 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002428 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002429 parser.add_option('--full', action='store_true',
2430 help='Reformat the full content of all touched files')
2431 parser.add_option('--dry-run', action='store_true',
2432 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002433 parser.add_option('--diff', action='store_true',
2434 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002435 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002436
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002437 # git diff generates paths against the root of the repository. Change
2438 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002439 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002440 if rel_base_path:
2441 os.chdir(rel_base_path)
2442
digit@chromium.org29e47272013-05-17 17:01:46 +00002443 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002444 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002445 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002446 # Only list the names of modified files.
2447 diff_cmd.append('--name-only')
2448 else:
2449 # Only generate context-less patches.
2450 diff_cmd.append('-U0')
2451
2452 # Grab the merge-base commit, i.e. the upstream commit of the current
2453 # branch when it was created or the last time it was rebased. This is
2454 # to cover the case where the user may have called "git fetch origin",
2455 # moving the origin branch to a newer commit, but hasn't rebased yet.
2456 upstream_commit = None
2457 cl = Changelist()
2458 upstream_branch = cl.GetUpstreamBranch()
2459 if upstream_branch:
2460 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2461 upstream_commit = upstream_commit.strip()
2462
2463 if not upstream_commit:
2464 DieWithError('Could not find base commit for this branch. '
2465 'Are you in detached state?')
2466
2467 diff_cmd.append(upstream_commit)
2468
2469 # Handle source file filtering.
2470 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002471 if args:
2472 for arg in args:
2473 if os.path.isdir(arg):
2474 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2475 elif os.path.isfile(arg):
2476 diff_cmd.append(arg)
2477 else:
2478 DieWithError('Argument "%s" is not a file or a directory' % arg)
2479 else:
2480 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002481 diff_output = RunGit(diff_cmd)
2482
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002483 top_dir = os.path.normpath(
2484 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2485
2486 # Locate the clang-format binary in the checkout
2487 try:
2488 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2489 except clang_format.NotFoundError, e:
2490 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002491
digit@chromium.org29e47272013-05-17 17:01:46 +00002492 if opts.full:
2493 # diff_output is a list of files to send to clang-format.
2494 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002495 if not files:
2496 print "Nothing to format."
2497 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002498 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002499 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002500 cmd.append('-i')
2501 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002502 if opts.diff:
2503 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002504 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002505 env = os.environ.copy()
2506 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002507 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002508 try:
2509 script = clang_format.FindClangFormatScriptInChromiumTree(
2510 'clang-format-diff.py')
2511 except clang_format.NotFoundError, e:
2512 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002513
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002514 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002515 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002516 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002517
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002518 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002519 if opts.diff:
2520 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002521 if opts.dry_run and len(stdout) > 0:
2522 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002523
2524 return 0
2525
2526
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002527class OptionParser(optparse.OptionParser):
2528 """Creates the option parse and add --verbose support."""
2529 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002530 optparse.OptionParser.__init__(
2531 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002532 self.add_option(
2533 '-v', '--verbose', action='count', default=0,
2534 help='Use 2 times for more debugging info')
2535
2536 def parse_args(self, args=None, values=None):
2537 options, args = optparse.OptionParser.parse_args(self, args, values)
2538 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2539 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2540 return options, args
2541
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002542
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002543def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002544 if sys.hexversion < 0x02060000:
2545 print >> sys.stderr, (
2546 '\nYour python version %s is unsupported, please upgrade.\n' %
2547 sys.version.split(' ', 1)[0])
2548 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002549
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002550 # Reload settings.
2551 global settings
2552 settings = Settings()
2553
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002554 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002555 dispatcher = subcommand.CommandDispatcher(__name__)
2556 try:
2557 return dispatcher.execute(OptionParser(), argv)
2558 except urllib2.HTTPError, e:
2559 if e.code != 500:
2560 raise
2561 DieWithError(
2562 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2563 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002564
2565
2566if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002567 # These affect sys.stdout so do it outside of main() to simplify mocks in
2568 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002569 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002570 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002571 sys.exit(main(sys.argv[1:]))