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