blob: eb6594dc48b89ed1d45343aeb186f1a4d603b0c5 [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
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000011import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000012import logging
13import optparse
14import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000015import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000017import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000022import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023
24try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000025 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026except ImportError:
27 pass
28
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000030from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031from third_party import upload
32import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000033import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000034import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000036import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000038import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000039import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import watchlists
41
maruel@chromium.org0633fb42013-08-16 20:06:14 +000042__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000044DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000045POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000046DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000047GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000048CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000050# Shortcut since it quickly becomes redundant.
51Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000052
maruel@chromium.orgddd59412011-11-30 14:20:38 +000053# Initialized in main()
54settings = None
55
56
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000058 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 sys.exit(1)
60
61
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000064 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000065 except subprocess2.CalledProcessError as e:
66 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000067 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000069 'Command "%s" failed.\n%s' % (
70 ' '.join(args), error_message or e.stdout or ''))
71 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
73
74def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000075 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000076 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000077
78
79def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000080 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000081 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000082 env = os.environ.copy()
83 # 'cat' is a magical git string that disables pagers on all platforms.
84 env['GIT_PAGER'] = 'cat'
85 out, code = subprocess2.communicate(['git'] + args,
86 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000087 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000088 return code, out[0]
89 except ValueError:
90 # When the subprocess fails, it returns None. That triggers a ValueError
91 # when trying to unpack the return value into (out, code).
92 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093
94
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000095def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000096 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000097 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000098 return (version.startswith(prefix) and
99 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000100
101
maruel@chromium.org90541732011-04-01 17:54:18 +0000102def ask_for_data(prompt):
103 try:
104 return raw_input(prompt)
105 except KeyboardInterrupt:
106 # Hide the exception.
107 sys.exit(1)
108
109
iannucci@chromium.org79540052012-10-19 23:15:26 +0000110def git_set_branch_value(key, value):
111 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000112 if not branch:
113 return
114
115 cmd = ['config']
116 if isinstance(value, int):
117 cmd.append('--int')
118 git_key = 'branch.%s.%s' % (branch, key)
119 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000120
121
122def git_get_branch_default(key, default):
123 branch = Changelist().GetBranch()
124 if branch:
125 git_key = 'branch.%s.%s' % (branch, key)
126 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
127 try:
128 return int(stdout.strip())
129 except ValueError:
130 pass
131 return default
132
133
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000134def add_git_similarity(parser):
135 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000136 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137 help='Sets the percentage that a pair of files need to match in order to'
138 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 parser.add_option(
140 '--find-copies', action='store_true',
141 help='Allows git to look for copies.')
142 parser.add_option(
143 '--no-find-copies', action='store_false', dest='find_copies',
144 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000145
146 old_parser_args = parser.parse_args
147 def Parse(args):
148 options, args = old_parser_args(args)
149
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000150 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000151 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000153 print('Note: Saving similarity of %d%% in git config.'
154 % options.similarity)
155 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 options.similarity = max(0, min(options.similarity, 100))
158
159 if options.find_copies is None:
160 options.find_copies = bool(
161 git_get_branch_default('git-find-copies', True))
162 else:
163 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164
165 print('Using %d%% similarity for rename/copy detection. '
166 'Override with --similarity.' % options.similarity)
167
168 return options, args
169 parser.parse_args = Parse
170
171
ukai@chromium.org259e4682012-10-25 07:36:33 +0000172def is_dirty_git_tree(cmd):
173 # Make sure index is up-to-date before running diff-index.
174 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
175 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
176 if dirty:
177 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
178 print 'Uncommitted files: (git diff-index --name-status HEAD)'
179 print dirty[:4096]
180 if len(dirty) > 4096:
181 print '... (run "git diff-index --name-status HEAD" to see full output).'
182 return True
183 return False
184
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000185
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000186def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
187 """Return the corresponding git ref if |base_url| together with |glob_spec|
188 matches the full |url|.
189
190 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
191 """
192 fetch_suburl, as_ref = glob_spec.split(':')
193 if allow_wildcards:
194 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
195 if glob_match:
196 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
197 # "branches/{472,597,648}/src:refs/remotes/svn/*".
198 branch_re = re.escape(base_url)
199 if glob_match.group(1):
200 branch_re += '/' + re.escape(glob_match.group(1))
201 wildcard = glob_match.group(2)
202 if wildcard == '*':
203 branch_re += '([^/]*)'
204 else:
205 # Escape and replace surrounding braces with parentheses and commas
206 # with pipe symbols.
207 wildcard = re.escape(wildcard)
208 wildcard = re.sub('^\\\\{', '(', wildcard)
209 wildcard = re.sub('\\\\,', '|', wildcard)
210 wildcard = re.sub('\\\\}$', ')', wildcard)
211 branch_re += wildcard
212 if glob_match.group(3):
213 branch_re += re.escape(glob_match.group(3))
214 match = re.match(branch_re, url)
215 if match:
216 return re.sub('\*$', match.group(1), as_ref)
217
218 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
219 if fetch_suburl:
220 full_url = base_url + '/' + fetch_suburl
221 else:
222 full_url = base_url
223 if full_url == url:
224 return as_ref
225 return None
226
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000227
iannucci@chromium.org79540052012-10-19 23:15:26 +0000228def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000229 """Prints statistics about the change to the user."""
230 # --no-ext-diff is broken in some versions of Git, so try to work around
231 # this by overriding the environment (but there is still a problem if the
232 # git config key "diff.external" is used).
233 env = os.environ.copy()
234 if 'GIT_EXTERNAL_DIFF' in env:
235 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000236 # 'cat' is a magical git string that disables pagers on all platforms.
237 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000238
239 if find_copies:
240 similarity_options = ['--find-copies-harder', '-l100000',
241 '-C%s' % similarity]
242 else:
243 similarity_options = ['-M%s' % similarity]
244
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000245 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000246 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000247 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000249
250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000251class Settings(object):
252 def __init__(self):
253 self.default_server = None
254 self.cc = None
255 self.root = None
256 self.is_git_svn = None
257 self.svn_branch = None
258 self.tree_status_url = None
259 self.viewvc_url = None
260 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000261 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000262 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000263
264 def LazyUpdateIfNeeded(self):
265 """Updates the settings from a codereview.settings file, if available."""
266 if not self.updated:
267 cr_settings_file = FindCodereviewSettingsFile()
268 if cr_settings_file:
269 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000270 self.updated = True
271 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272 self.updated = True
273
274 def GetDefaultServerUrl(self, error_ok=False):
275 if not self.default_server:
276 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000277 self.default_server = gclient_utils.UpgradeToHttps(
278 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000279 if error_ok:
280 return self.default_server
281 if not self.default_server:
282 error_message = ('Could not find settings file. You must configure '
283 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000284 self.default_server = gclient_utils.UpgradeToHttps(
285 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286 return self.default_server
287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288 def GetRoot(self):
289 if not self.root:
290 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
291 return self.root
292
293 def GetIsGitSvn(self):
294 """Return true if this repo looks like it's using git-svn."""
295 if self.is_git_svn is None:
296 # If you have any "svn-remote.*" config keys, we think you're using svn.
297 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000298 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299 return self.is_git_svn
300
301 def GetSVNBranch(self):
302 if self.svn_branch is None:
303 if not self.GetIsGitSvn():
304 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
305
306 # Try to figure out which remote branch we're based on.
307 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000308 # 1) iterate through our branch history and find the svn URL.
309 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310
311 # regexp matching the git-svn line that contains the URL.
312 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
313
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000314 env = os.environ.copy()
315 # 'cat' is a magical git string that disables pagers on all platforms.
316 env['GIT_PAGER'] = 'cat'
317
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000318 # We don't want to go through all of history, so read a line from the
319 # pipe at a time.
320 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000321 cmd = ['git', 'log', '-100', '--pretty=medium']
322 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000323 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000324 for line in proc.stdout:
325 match = git_svn_re.match(line)
326 if match:
327 url = match.group(1)
328 proc.stdout.close() # Cut pipe.
329 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000330
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000331 if url:
332 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
333 remotes = RunGit(['config', '--get-regexp',
334 r'^svn-remote\..*\.url']).splitlines()
335 for remote in remotes:
336 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000338 remote = match.group(1)
339 base_url = match.group(2)
340 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000341 ['config', 'svn-remote.%s.fetch' % remote],
342 error_ok=True).strip()
343 if fetch_spec:
344 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
345 if self.svn_branch:
346 break
347 branch_spec = RunGit(
348 ['config', 'svn-remote.%s.branches' % remote],
349 error_ok=True).strip()
350 if branch_spec:
351 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
352 if self.svn_branch:
353 break
354 tag_spec = RunGit(
355 ['config', 'svn-remote.%s.tags' % remote],
356 error_ok=True).strip()
357 if tag_spec:
358 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
359 if self.svn_branch:
360 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000361
362 if not self.svn_branch:
363 DieWithError('Can\'t guess svn branch -- try specifying it on the '
364 'command line')
365
366 return self.svn_branch
367
368 def GetTreeStatusUrl(self, error_ok=False):
369 if not self.tree_status_url:
370 error_message = ('You must configure your tree status URL by running '
371 '"git cl config".')
372 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
373 error_ok=error_ok,
374 error_message=error_message)
375 return self.tree_status_url
376
377 def GetViewVCUrl(self):
378 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000379 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000380 return self.viewvc_url
381
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000382 def GetDefaultCCList(self):
383 return self._GetConfig('rietveld.cc', error_ok=True)
384
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000385 def GetDefaultPrivateFlag(self):
386 return self._GetConfig('rietveld.private', error_ok=True)
387
ukai@chromium.orge8077812012-02-03 03:41:46 +0000388 def GetIsGerrit(self):
389 """Return true if this repo is assosiated with gerrit code review system."""
390 if self.is_gerrit is None:
391 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
392 return self.is_gerrit
393
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000394 def GetGitEditor(self):
395 """Return the editor specified in the git config, or None if none is."""
396 if self.git_editor is None:
397 self.git_editor = self._GetConfig('core.editor', error_ok=True)
398 return self.git_editor or None
399
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000400 def _GetConfig(self, param, **kwargs):
401 self.LazyUpdateIfNeeded()
402 return RunGit(['config', param], **kwargs).strip()
403
404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000405def ShortBranchName(branch):
406 """Convert a name like 'refs/heads/foo' to just 'foo'."""
407 return branch.replace('refs/heads/', '')
408
409
410class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000411 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000412 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000413 global settings
414 if not settings:
415 # Happens when git_cl.py is used as a utility library.
416 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000417 settings.GetDefaultServerUrl()
418 self.branchref = branchref
419 if self.branchref:
420 self.branch = ShortBranchName(self.branchref)
421 else:
422 self.branch = None
423 self.rietveld_server = None
424 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000425 self.lookedup_issue = False
426 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000427 self.has_description = False
428 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000429 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000431 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000432 self.cc = None
433 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000434 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000435 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000436
437 def GetCCList(self):
438 """Return the users cc'd on this CL.
439
440 Return is a string suitable for passing to gcl with the --cc flag.
441 """
442 if self.cc is None:
443 base_cc = settings .GetDefaultCCList()
444 more_cc = ','.join(self.watchers)
445 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
446 return self.cc
447
448 def SetWatchers(self, watchers):
449 """Set the list of email addresses that should be cc'd based on the changed
450 files in this CL.
451 """
452 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453
454 def GetBranch(self):
455 """Returns the short branch name, e.g. 'master'."""
456 if not self.branch:
457 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
458 self.branch = ShortBranchName(self.branchref)
459 return self.branch
460
461 def GetBranchRef(self):
462 """Returns the full branch name, e.g. 'refs/heads/master'."""
463 self.GetBranch() # Poke the lazy loader.
464 return self.branchref
465
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000466 @staticmethod
467 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000468 """Returns a tuple containg remote and remote ref,
469 e.g. 'origin', 'refs/heads/master'
470 """
471 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
473 error_ok=True).strip()
474 if upstream_branch:
475 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
476 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000477 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
478 error_ok=True).strip()
479 if upstream_branch:
480 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000482 # Fall back on trying a git-svn upstream branch.
483 if settings.GetIsGitSvn():
484 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000485 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000486 # Else, try to guess the origin remote.
487 remote_branches = RunGit(['branch', '-r']).split()
488 if 'origin/master' in remote_branches:
489 # Fall back on origin/master if it exits.
490 remote = 'origin'
491 upstream_branch = 'refs/heads/master'
492 elif 'origin/trunk' in remote_branches:
493 # Fall back on origin/trunk if it exists. Generally a shared
494 # git-svn clone
495 remote = 'origin'
496 upstream_branch = 'refs/heads/trunk'
497 else:
498 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000499Either pass complete "git diff"-style arguments, like
500 git cl upload origin/master
501or verify this branch is set up to track another (via the --track argument to
502"git checkout -b ...").""")
503
504 return remote, upstream_branch
505
506 def GetUpstreamBranch(self):
507 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000508 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000509 if remote is not '.':
510 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
511 self.upstream_branch = upstream_branch
512 return self.upstream_branch
513
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000514 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000515 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 remote, branch = None, self.GetBranch()
517 seen_branches = set()
518 while branch not in seen_branches:
519 seen_branches.add(branch)
520 remote, branch = self.FetchUpstreamTuple(branch)
521 branch = ShortBranchName(branch)
522 if remote != '.' or branch.startswith('refs/remotes'):
523 break
524 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000525 remotes = RunGit(['remote'], error_ok=True).split()
526 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000527 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000528 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000529 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000530 logging.warning('Could not determine which remote this change is '
531 'associated with, so defaulting to "%s". This may '
532 'not be what you want. You may prevent this message '
533 'by running "git svn info" as documented here: %s',
534 self._remote,
535 GIT_INSTRUCTIONS_URL)
536 else:
537 logging.warn('Could not determine which remote this change is '
538 'associated with. You may prevent this message by '
539 'running "git svn info" as documented here: %s',
540 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000541 branch = 'HEAD'
542 if branch.startswith('refs/remotes'):
543 self._remote = (remote, branch)
544 else:
545 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000546 return self._remote
547
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000548 def GitSanityChecks(self, upstream_git_obj):
549 """Checks git repo status and ensures diff is from local commits."""
550
551 # Verify the commit we're diffing against is in our current branch.
552 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
553 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
554 if upstream_sha != common_ancestor:
555 print >> sys.stderr, (
556 'ERROR: %s is not in the current branch. You may need to rebase '
557 'your tracking branch' % upstream_sha)
558 return False
559
560 # List the commits inside the diff, and verify they are all local.
561 commits_in_diff = RunGit(
562 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
563 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
564 remote_branch = remote_branch.strip()
565 if code != 0:
566 _, remote_branch = self.GetRemoteBranch()
567
568 commits_in_remote = RunGit(
569 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
570
571 common_commits = set(commits_in_diff) & set(commits_in_remote)
572 if common_commits:
573 print >> sys.stderr, (
574 'ERROR: Your diff contains %d commits already in %s.\n'
575 'Run "git log --oneline %s..HEAD" to get a list of commits in '
576 'the diff. If you are using a custom git flow, you can override'
577 ' the reference used for this check with "git config '
578 'gitcl.remotebranch <git-ref>".' % (
579 len(common_commits), remote_branch, upstream_git_obj))
580 return False
581 return True
582
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000583 def GetGitBaseUrlFromConfig(self):
584 """Return the configured base URL from branch.<branchname>.baseurl.
585
586 Returns None if it is not set.
587 """
588 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
589 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000590
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591 def GetRemoteUrl(self):
592 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
593
594 Returns None if there is no remote.
595 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000596 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
598
599 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000600 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000601 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000603 self.issue = int(issue) or None if issue else None
604 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 return self.issue
606
607 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000608 if not self.rietveld_server:
609 # If we're on a branch then get the server potentially associated
610 # with that branch.
611 if self.GetIssue():
612 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
613 ['config', self._RietveldServer()], error_ok=True).strip())
614 if not self.rietveld_server:
615 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 return self.rietveld_server
617
618 def GetIssueURL(self):
619 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000620 if not self.GetIssue():
621 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
623
624 def GetDescription(self, pretty=False):
625 if not self.has_description:
626 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000627 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000628 try:
629 self.description = self.RpcServer().get_description(issue).strip()
630 except urllib2.HTTPError, e:
631 if e.code == 404:
632 DieWithError(
633 ('\nWhile fetching the description for issue %d, received a '
634 '404 (not found)\n'
635 'error. It is likely that you deleted this '
636 'issue on the server. If this is the\n'
637 'case, please run\n\n'
638 ' git cl issue 0\n\n'
639 'to clear the association with the deleted issue. Then run '
640 'this command again.') % issue)
641 else:
642 DieWithError(
643 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644 self.has_description = True
645 if pretty:
646 wrapper = textwrap.TextWrapper()
647 wrapper.initial_indent = wrapper.subsequent_indent = ' '
648 return wrapper.fill(self.description)
649 return self.description
650
651 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000652 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000653 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000654 patchset = RunGit(['config', self._PatchsetSetting()],
655 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000656 self.patchset = int(patchset) or None if patchset else None
657 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658 return self.patchset
659
660 def SetPatchset(self, patchset):
661 """Set this branch's patchset. If patchset=0, clears the patchset."""
662 if patchset:
663 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000664 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665 else:
666 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000667 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000668 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000670 def GetMostRecentPatchset(self):
671 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000672
673 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000674 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000675 '/download/issue%s_%s.diff' % (issue, patchset))
676
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000677 def GetIssueProperties(self):
678 if self._props is None:
679 issue = self.GetIssue()
680 if not issue:
681 self._props = {}
682 else:
683 self._props = self.RpcServer().get_issue_properties(issue, True)
684 return self._props
685
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000686 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000687 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000688
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689 def SetIssue(self, issue):
690 """Set this branch's issue. If issue=0, clears the issue."""
691 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000692 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000693 RunGit(['config', self._IssueSetting(), str(issue)])
694 if self.rietveld_server:
695 RunGit(['config', self._RietveldServer(), self.rietveld_server])
696 else:
697 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000698 self.issue = None
699 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000701 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000702 if not self.GitSanityChecks(upstream_branch):
703 DieWithError('\nGit sanity check failure')
704
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000705 env = os.environ.copy()
706 # 'cat' is a magical git string that disables pagers on all platforms.
707 env['GIT_PAGER'] = 'cat'
708
709 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000710 if not root:
711 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000712 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000713
714 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000715 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000716 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000717 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000718 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000719 except subprocess2.CalledProcessError:
720 DieWithError(
721 ('\nFailed to diff against upstream branch %s!\n\n'
722 'This branch probably doesn\'t exist anymore. To reset the\n'
723 'tracking branch, please run\n'
724 ' git branch --set-upstream %s trunk\n'
725 'replacing trunk with origin/master or the relevant branch') %
726 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000727
maruel@chromium.org52424302012-08-29 15:14:30 +0000728 issue = self.GetIssue()
729 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000730 if issue:
731 description = self.GetDescription()
732 else:
733 # If the change was never uploaded, use the log messages of all commits
734 # up to the branch point, as git cl upload will prefill the description
735 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000736 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000737 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000738 '%s...' % (upstream_branch)],
739 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000740
741 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000742 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000743 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000744 name,
745 description,
746 absroot,
747 files,
748 issue,
749 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000750 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000751
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000752 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000753 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000754
755 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000756 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000757 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000758 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000759 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000760 except presubmit_support.PresubmitFailure, e:
761 DieWithError(
762 ('%s\nMaybe your depot_tools is out of date?\n'
763 'If all fails, contact maruel@') % e)
764
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000765 def UpdateDescription(self, description):
766 self.description = description
767 return self.RpcServer().update_description(
768 self.GetIssue(), self.description)
769
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000771 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000772 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000774 def SetFlag(self, flag, value):
775 """Patchset must match."""
776 if not self.GetPatchset():
777 DieWithError('The patchset needs to match. Send another patchset.')
778 try:
779 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000780 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000781 except urllib2.HTTPError, e:
782 if e.code == 404:
783 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
784 if e.code == 403:
785 DieWithError(
786 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
787 'match?') % (self.GetIssue(), self.GetPatchset()))
788 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000790 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 """Returns an upload.RpcServer() to access this review's rietveld instance.
792 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000793 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000794 self._rpc_server = rietveld.CachingRietveld(
795 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000796 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797
798 def _IssueSetting(self):
799 """Return the git setting that stores this change's issue."""
800 return 'branch.%s.rietveldissue' % self.GetBranch()
801
802 def _PatchsetSetting(self):
803 """Return the git setting that stores this change's most recent patchset."""
804 return 'branch.%s.rietveldpatchset' % self.GetBranch()
805
806 def _RietveldServer(self):
807 """Returns the git setting that stores this change's rietveld server."""
808 return 'branch.%s.rietveldserver' % self.GetBranch()
809
810
811def GetCodereviewSettingsInteractively():
812 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000813 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 server = settings.GetDefaultServerUrl(error_ok=True)
815 prompt = 'Rietveld server (host[:port])'
816 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000817 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 if not server and not newserver:
819 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000820 if newserver:
821 newserver = gclient_utils.UpgradeToHttps(newserver)
822 if newserver != server:
823 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000825 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 prompt = caption
827 if initial:
828 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000829 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830 if new_val == 'x':
831 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000832 elif new_val:
833 if is_url:
834 new_val = gclient_utils.UpgradeToHttps(new_val)
835 if new_val != initial:
836 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000838 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000839 SetProperty(settings.GetDefaultPrivateFlag(),
840 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000842 'tree-status-url', False)
843 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
845 # TODO: configure a default branch to diff against, rather than this
846 # svn-based hackery.
847
848
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000849class ChangeDescription(object):
850 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000851 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000852
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000853 def __init__(self, description):
854 self._description = (description or '').strip()
855
856 @property
857 def description(self):
858 return self._description
859
860 def update_reviewers(self, reviewers):
861 """Rewrites the R=/TBR= line(s) as a single line."""
862 assert isinstance(reviewers, list), reviewers
863 if not reviewers:
864 return
865 regexp = re.compile(self.R_LINE, re.MULTILINE)
866 matches = list(regexp.finditer(self._description))
867 is_tbr = any(m.group(1) == 'TBR' for m in matches)
868 if len(matches) > 1:
869 # Erase all except the first one.
870 for i in xrange(len(matches) - 1, 0, -1):
871 self._description = (
872 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000873 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000874
875 if is_tbr:
876 new_r_line = 'TBR=' + ', '.join(reviewers)
877 else:
878 new_r_line = 'R=' + ', '.join(reviewers)
879
880 if matches:
881 self._description = (
882 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000883 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000884 else:
885 self.append_footer(new_r_line)
886
887 def prompt(self):
888 """Asks the user to update the description."""
889 self._description = (
890 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000891 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000892 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000893 '#--------------------This line is 72 characters long'
894 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000895 ) + self._description
896
897 if '\nBUG=' not in self._description:
898 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000899 content = gclient_utils.RunEditor(self._description, True,
900 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000901 if not content:
902 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000903
904 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000905 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000906 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000907 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000908 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000909
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000910 def append_footer(self, line):
911 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
912 if self._description:
913 if '\n' not in self._description:
914 self._description += '\n'
915 else:
916 last_line = self._description.rsplit('\n', 1)[1]
917 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
918 not presubmit_support.Change.TAG_LINE_RE.match(line)):
919 self._description += '\n'
920 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000921
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000922 def get_reviewers(self):
923 """Retrieves the list of reviewers."""
924 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000925 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000926 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000927
928
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000929def get_approving_reviewers(props):
930 """Retrieves the reviewers that approved a CL from the issue properties with
931 messages.
932
933 Note that the list may contain reviewers that are not committer, thus are not
934 considered by the CQ.
935 """
936 return sorted(
937 set(
938 message['sender']
939 for message in props['messages']
940 if message['approval'] and message['sender'] in props['reviewers']
941 )
942 )
943
944
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945def FindCodereviewSettingsFile(filename='codereview.settings'):
946 """Finds the given file starting in the cwd and going up.
947
948 Only looks up to the top of the repository unless an
949 'inherit-review-settings-ok' file exists in the root of the repository.
950 """
951 inherit_ok_file = 'inherit-review-settings-ok'
952 cwd = os.getcwd()
953 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
954 if os.path.isfile(os.path.join(root, inherit_ok_file)):
955 root = '/'
956 while True:
957 if filename in os.listdir(cwd):
958 if os.path.isfile(os.path.join(cwd, filename)):
959 return open(os.path.join(cwd, filename))
960 if cwd == root:
961 break
962 cwd = os.path.dirname(cwd)
963
964
965def LoadCodereviewSettingsFromFile(fileobj):
966 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000967 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 def SetProperty(name, setting, unset_error_ok=False):
970 fullname = 'rietveld.' + name
971 if setting in keyvals:
972 RunGit(['config', fullname, keyvals[setting]])
973 else:
974 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
975
976 SetProperty('server', 'CODE_REVIEW_SERVER')
977 # Only server setting is required. Other settings can be absent.
978 # In that case, we ignore errors raised during option deletion attempt.
979 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000980 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
982 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
983
ukai@chromium.orge8077812012-02-03 03:41:46 +0000984 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
985 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
986 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000987
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000988 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
989 #should be of the form
990 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
991 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
992 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
993 keyvals['ORIGIN_URL_CONFIG']])
994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000996def urlretrieve(source, destination):
997 """urllib is broken for SSL connections via a proxy therefore we
998 can't use urllib.urlretrieve()."""
999 with open(destination, 'w') as f:
1000 f.write(urllib2.urlopen(source).read())
1001
1002
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001003def DownloadHooks(force):
1004 """downloads hooks
1005
1006 Args:
1007 force: True to update hooks. False to install hooks if not present.
1008 """
1009 if not settings.GetIsGerrit():
1010 return
1011 server_url = settings.GetDefaultServerUrl()
1012 src = '%s/tools/hooks/commit-msg' % server_url
1013 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1014 if not os.access(dst, os.X_OK):
1015 if os.path.exists(dst):
1016 if not force:
1017 return
1018 os.remove(dst)
1019 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001020 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001021 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1022 except Exception:
1023 if os.path.exists(dst):
1024 os.remove(dst)
1025 DieWithError('\nFailed to download hooks from %s' % src)
1026
1027
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001028@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001030 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001032 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 if len(args) == 0:
1034 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001035 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 return 0
1037
1038 url = args[0]
1039 if not url.endswith('codereview.settings'):
1040 url = os.path.join(url, 'codereview.settings')
1041
1042 # Load code review settings and download hooks (if available).
1043 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001044 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 return 0
1046
1047
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001048def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001049 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001050 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1051 branch = ShortBranchName(branchref)
1052 _, args = parser.parse_args(args)
1053 if not args:
1054 print("Current base-url:")
1055 return RunGit(['config', 'branch.%s.base-url' % branch],
1056 error_ok=False).strip()
1057 else:
1058 print("Setting base-url to %s" % args[0])
1059 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1060 error_ok=False).strip()
1061
1062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001064 """Show status of changelists.
1065
1066 Colors are used to tell the state of the CL unless --fast is used:
1067 - Green LGTM'ed
1068 - Blue waiting for review
1069 - Yellow waiting for you to reply to review
1070 - Red not sent for review or broken
1071 - Cyan was committed, branch can be deleted
1072
1073 Also see 'git cl comments'.
1074 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 parser.add_option('--field',
1076 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001077 parser.add_option('-f', '--fast', action='store_true',
1078 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001079 (options, args) = parser.parse_args(args)
1080
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001082 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 if options.field.startswith('desc'):
1084 print cl.GetDescription()
1085 elif options.field == 'id':
1086 issueid = cl.GetIssue()
1087 if issueid:
1088 print issueid
1089 elif options.field == 'patch':
1090 patchset = cl.GetPatchset()
1091 if patchset:
1092 print patchset
1093 elif options.field == 'url':
1094 url = cl.GetIssueURL()
1095 if url:
1096 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001097 return 0
1098
1099 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1100 if not branches:
1101 print('No local branch found.')
1102 return 0
1103
1104 changes = (Changelist(branchref=b) for b in branches.splitlines())
1105 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1106 alignment = max(5, max(len(b) for b in branches))
1107 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001108 # Adhoc thread pool to request data concurrently.
1109 output = Queue.Queue()
1110
1111 # Silence upload.py otherwise it becomes unweldly.
1112 upload.verbosity = 0
1113
1114 if not options.fast:
1115 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001116 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001117 c = Changelist(branchref=b)
1118 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001119 props = {}
1120 r = None
1121 if i:
1122 try:
1123 props = c.GetIssueProperties()
1124 r = c.GetApprovingReviewers() if i else None
1125 except urllib2.HTTPError:
1126 # The issue probably doesn't exist anymore.
1127 i += ' (broken)'
1128
1129 msgs = props.get('messages') or []
1130
1131 if not i:
1132 color = Fore.WHITE
1133 elif props.get('closed'):
1134 # Issue is closed.
1135 color = Fore.CYAN
1136 elif r:
1137 # Was LGTM'ed.
1138 color = Fore.GREEN
1139 elif not msgs:
1140 # No message was sent.
1141 color = Fore.RED
1142 elif msgs[-1]['sender'] != props.get('owner_email'):
1143 color = Fore.YELLOW
1144 else:
1145 color = Fore.BLUE
1146 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147
1148 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1149 for t in threads:
1150 t.daemon = True
1151 t.start()
1152 else:
1153 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1154 for b in branches:
1155 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001156 url = c.GetIssueURL()
1157 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001158
1159 tmp = {}
1160 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001161 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001162 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001163 b, i, color = output.get()
1164 tmp[b] = (i, color)
1165 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001166 reset = Fore.RESET
1167 if not sys.stdout.isatty():
1168 color = ''
1169 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001170 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001171 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001172
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001173 cl = Changelist()
1174 print
1175 print 'Current branch:',
1176 if not cl.GetIssue():
1177 print 'no issue assigned.'
1178 return 0
1179 print cl.GetBranch()
1180 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1181 print 'Issue description:'
1182 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 return 0
1184
1185
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001186@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001188 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189
1190 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001191 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001192 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193
1194 cl = Changelist()
1195 if len(args) > 0:
1196 try:
1197 issue = int(args[0])
1198 except ValueError:
1199 DieWithError('Pass a number to set the issue or none to list it.\n'
1200 'Maybe you want to run git cl status?')
1201 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001202 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 return 0
1204
1205
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001206def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001207 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001208 (_, args) = parser.parse_args(args)
1209 if args:
1210 parser.error('Unsupported argument: %s' % args)
1211
1212 cl = Changelist()
1213 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001214 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001215 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001216 if message['disapproval']:
1217 color = Fore.RED
1218 elif message['approval']:
1219 color = Fore.GREEN
1220 elif message['sender'] == data['owner_email']:
1221 color = Fore.MAGENTA
1222 else:
1223 color = Fore.BLUE
1224 print '\n%s%s %s%s' % (
1225 color, message['date'].split('.', 1)[0], message['sender'],
1226 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001227 if message['text'].strip():
1228 print '\n'.join(' ' + l for l in message['text'].splitlines())
1229 return 0
1230
1231
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001232def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001233 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001234 cl = Changelist()
1235 if not cl.GetIssue():
1236 DieWithError('This branch has no associated changelist.')
1237 description = ChangeDescription(cl.GetDescription())
1238 description.prompt()
1239 cl.UpdateDescription(description.description)
1240 return 0
1241
1242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243def CreateDescriptionFromLog(args):
1244 """Pulls out the commit log to use as a base for the CL description."""
1245 log_args = []
1246 if len(args) == 1 and not args[0].endswith('.'):
1247 log_args = [args[0] + '..']
1248 elif len(args) == 1 and args[0].endswith('...'):
1249 log_args = [args[0][:-1]]
1250 elif len(args) == 2:
1251 log_args = [args[0] + '..' + args[1]]
1252 else:
1253 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001254 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255
1256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001258 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001259 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001261 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001262 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 (options, args) = parser.parse_args(args)
1264
ukai@chromium.org259e4682012-10-25 07:36:33 +00001265 if not options.force and is_dirty_git_tree('presubmit'):
1266 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 return 1
1268
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001269 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 if args:
1271 base_branch = args[0]
1272 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 # Default to diffing against the common ancestor of the upstream branch.
1274 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001276 cl.RunHook(
1277 committing=not options.upload,
1278 may_prompt=False,
1279 verbose=options.verbose,
1280 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001281 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282
1283
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001284def AddChangeIdToCommitMessage(options, args):
1285 """Re-commits using the current message, assumes the commit hook is in
1286 place.
1287 """
1288 log_desc = options.message or CreateDescriptionFromLog(args)
1289 git_command = ['commit', '--amend', '-m', log_desc]
1290 RunGit(git_command)
1291 new_log_desc = CreateDescriptionFromLog(args)
1292 if CHANGE_ID in new_log_desc:
1293 print 'git-cl: Added Change-Id to commit message.'
1294 else:
1295 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1296
1297
ukai@chromium.orge8077812012-02-03 03:41:46 +00001298def GerritUpload(options, args, cl):
1299 """upload the current branch to gerrit."""
1300 # We assume the remote called "origin" is the one we want.
1301 # It is probably not worthwhile to support different workflows.
1302 remote = 'origin'
1303 branch = 'master'
1304 if options.target_branch:
1305 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001307 change_desc = ChangeDescription(
1308 options.message or CreateDescriptionFromLog(args))
1309 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001310 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001312 if CHANGE_ID not in change_desc.description:
1313 AddChangeIdToCommitMessage(options, args)
1314 if options.reviewers:
1315 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316
ukai@chromium.orge8077812012-02-03 03:41:46 +00001317 receive_options = []
1318 cc = cl.GetCCList().split(',')
1319 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001320 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001321 cc = filter(None, cc)
1322 if cc:
1323 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001324 if change_desc.get_reviewers():
1325 receive_options.extend(
1326 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327
ukai@chromium.orge8077812012-02-03 03:41:46 +00001328 git_command = ['push']
1329 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001330 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001331 ' '.join(receive_options))
1332 git_command += [remote, 'HEAD:refs/for/' + branch]
1333 RunGit(git_command)
1334 # TODO(ukai): parse Change-Id: and set issue number?
1335 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001336
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337
ukai@chromium.orge8077812012-02-03 03:41:46 +00001338def RietveldUpload(options, args, cl):
1339 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1341 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342 if options.emulate_svn_auto_props:
1343 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344
1345 change_desc = None
1346
1347 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001348 if options.title:
1349 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001350 if options.message:
1351 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001352 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 print ("This branch is associated with issue %s. "
1354 "Adding patch to that issue." % cl.GetIssue())
1355 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001356 if options.title:
1357 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001358 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001359 change_desc = ChangeDescription(message)
1360 if options.reviewers:
1361 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001362 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001363 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001364
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001365 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 print "Description is empty; aborting."
1367 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001368
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001369 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001370 if change_desc.get_reviewers():
1371 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001372 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001373 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001374 DieWithError("Must specify reviewers to send email.")
1375 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001376 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001377 if cc:
1378 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001380 if options.private or settings.GetDefaultPrivateFlag() == "True":
1381 upload_args.append('--private')
1382
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001383 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001384 if not options.find_copies:
1385 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001386
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 # Include the upstream repo's URL in the change -- this is useful for
1388 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001389 remote_url = cl.GetGitBaseUrlFromConfig()
1390 if not remote_url:
1391 if settings.GetIsGitSvn():
1392 # URL is dependent on the current directory.
1393 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1394 if data:
1395 keys = dict(line.split(': ', 1) for line in data.splitlines()
1396 if ': ' in line)
1397 remote_url = keys.get('URL', None)
1398 else:
1399 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1400 remote_url = (cl.GetRemoteUrl() + '@'
1401 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 if remote_url:
1403 upload_args.extend(['--base_url', remote_url])
1404
1405 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001406 upload_args = ['upload'] + upload_args + args
1407 logging.info('upload.RealMain(%s)', upload_args)
1408 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001409 issue = int(issue)
1410 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001411 except KeyboardInterrupt:
1412 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 except:
1414 # If we got an exception after the user typed a description for their
1415 # change, back up the description before re-raising.
1416 if change_desc:
1417 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1418 print '\nGot exception while uploading -- saving description to %s\n' \
1419 % backup_path
1420 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001421 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 backup_file.close()
1423 raise
1424
1425 if not cl.GetIssue():
1426 cl.SetIssue(issue)
1427 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001428
1429 if options.use_commit_queue:
1430 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 return 0
1432
1433
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001434def cleanup_list(l):
1435 """Fixes a list so that comma separated items are put as individual items.
1436
1437 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1438 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1439 """
1440 items = sum((i.split(',') for i in l), [])
1441 stripped_items = (i.strip() for i in items)
1442 return sorted(filter(None, stripped_items))
1443
1444
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001445@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001446def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001447 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001448 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1449 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001450 parser.add_option('--bypass-watchlists', action='store_true',
1451 dest='bypass_watchlists',
1452 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001453 parser.add_option('-f', action='store_true', dest='force',
1454 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001455 parser.add_option('-m', dest='message', help='message for patchset')
1456 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001457 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001458 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001459 help='reviewer email addresses')
1460 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001461 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001462 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001463 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001464 help='send email to reviewer immediately')
1465 parser.add_option("--emulate_svn_auto_props", action="store_true",
1466 dest="emulate_svn_auto_props",
1467 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001468 parser.add_option('-c', '--use-commit-queue', action='store_true',
1469 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001470 parser.add_option('--private', action='store_true',
1471 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001472 parser.add_option('--target_branch',
1473 help='When uploading to gerrit, remote branch to '
1474 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001475 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001476 (options, args) = parser.parse_args(args)
1477
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001478 if options.target_branch and not settings.GetIsGerrit():
1479 parser.error('Use --target_branch for non gerrit repository.')
1480
ukai@chromium.org259e4682012-10-25 07:36:33 +00001481 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001482 return 1
1483
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001484 options.reviewers = cleanup_list(options.reviewers)
1485 options.cc = cleanup_list(options.cc)
1486
ukai@chromium.orge8077812012-02-03 03:41:46 +00001487 cl = Changelist()
1488 if args:
1489 # TODO(ukai): is it ok for gerrit case?
1490 base_branch = args[0]
1491 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001492 # Default to diffing against common ancestor of upstream branch
1493 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001494 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001495
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001496 # Apply watchlists on upload.
1497 change = cl.GetChange(base_branch, None)
1498 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1499 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001500 if not options.bypass_watchlists:
1501 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001502
ukai@chromium.orge8077812012-02-03 03:41:46 +00001503 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001504 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001505 may_prompt=not options.force,
1506 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001507 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001508 if not hook_results.should_continue():
1509 return 1
1510 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001511 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001512
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001513 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001514 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001515 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001516 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001517 print ('The last upload made from this repository was patchset #%d but '
1518 'the most recent patchset on the server is #%d.'
1519 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001520 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1521 'from another machine or branch the patch you\'re uploading now '
1522 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001523 ask_for_data('About to upload; enter to confirm.')
1524
iannucci@chromium.org79540052012-10-19 23:15:26 +00001525 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001526 if settings.GetIsGerrit():
1527 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001528 ret = RietveldUpload(options, args, cl)
1529 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001530 git_set_branch_value('last-upload-hash',
1531 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001532
1533 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001534
1535
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001536def IsSubmoduleMergeCommit(ref):
1537 # When submodules are added to the repo, we expect there to be a single
1538 # non-git-svn merge commit at remote HEAD with a signature comment.
1539 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001540 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001541 return RunGit(cmd) != ''
1542
1543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544def SendUpstream(parser, args, cmd):
1545 """Common code for CmdPush and CmdDCommit
1546
1547 Squashed commit into a single.
1548 Updates changelog with metadata (e.g. pointer to review).
1549 Pushes/dcommits the code upstream.
1550 Updates review and closes.
1551 """
1552 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1553 help='bypass upload presubmit hook')
1554 parser.add_option('-m', dest='message',
1555 help="override review description")
1556 parser.add_option('-f', action='store_true', dest='force',
1557 help="force yes to questions (don't prompt)")
1558 parser.add_option('-c', dest='contributor',
1559 help="external contributor for patch (appended to " +
1560 "description and used as author for git). Should be " +
1561 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001562 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 (options, args) = parser.parse_args(args)
1564 cl = Changelist()
1565
1566 if not args or cmd == 'push':
1567 # Default to merging against our best guess of the upstream branch.
1568 args = [cl.GetUpstreamBranch()]
1569
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001570 if options.contributor:
1571 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1572 print "Please provide contibutor as 'First Last <email@example.com>'"
1573 return 1
1574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001576 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577
ukai@chromium.org259e4682012-10-25 07:36:33 +00001578 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579 return 1
1580
1581 # This rev-list syntax means "show all commits not in my branch that
1582 # are in base_branch".
1583 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1584 base_branch]).splitlines()
1585 if upstream_commits:
1586 print ('Base branch "%s" has %d commits '
1587 'not in this branch.' % (base_branch, len(upstream_commits)))
1588 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1589 return 1
1590
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001591 # This is the revision `svn dcommit` will commit on top of.
1592 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1593 '--pretty=format:%H'])
1594
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001595 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001596 # If the base_head is a submodule merge commit, the first parent of the
1597 # base_head should be a git-svn commit, which is what we're interested in.
1598 base_svn_head = base_branch
1599 if base_has_submodules:
1600 base_svn_head += '^1'
1601
1602 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001603 if extra_commits:
1604 print ('This branch has %d additional commits not upstreamed yet.'
1605 % len(extra_commits.splitlines()))
1606 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1607 'before attempting to %s.' % (base_branch, cmd))
1608 return 1
1609
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001610 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001611 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001612 author = None
1613 if options.contributor:
1614 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001615 hook_results = cl.RunHook(
1616 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001617 may_prompt=not options.force,
1618 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001619 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001620 if not hook_results.should_continue():
1621 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622
1623 if cmd == 'dcommit':
1624 # Check the tree status if the tree status URL is set.
1625 status = GetTreeStatus()
1626 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001627 print('The tree is closed. Please wait for it to reopen. Use '
1628 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001629 return 1
1630 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001631 print('Unable to determine tree status. Please verify manually and '
1632 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001633 else:
1634 breakpad.SendStack(
1635 'GitClHooksBypassedCommit',
1636 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001637 (cl.GetRietveldServer(), cl.GetIssue()),
1638 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001640 change_desc = ChangeDescription(options.message)
1641 if not change_desc.description and cl.GetIssue():
1642 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001644 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001645 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001646 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001647 else:
1648 print 'No description set.'
1649 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1650 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001652 # Keep a separate copy for the commit message, because the commit message
1653 # contains the link to the Rietveld issue, while the Rietveld message contains
1654 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001655 # Keep a separate copy for the commit message.
1656 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001657 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001658
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001659 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001660 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001661 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001663 commit_desc.append_footer('Patch from %s.' % options.contributor)
1664
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001665 print('Description:')
1666 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001667
1668 branches = [base_branch, cl.GetBranchRef()]
1669 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001670 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001671 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001673 # We want to squash all this branch's commits into one commit with the proper
1674 # description. We do this by doing a "reset --soft" to the base branch (which
1675 # keeps the working copy the same), then dcommitting that. If origin/master
1676 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1677 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001678 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001679 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1680 # Delete the branches if they exist.
1681 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1682 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1683 result = RunGitWithCode(showref_cmd)
1684 if result[0] == 0:
1685 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686
1687 # We might be in a directory that's present in this branch but not in the
1688 # trunk. Move up to the top of the tree so that git commands that expect a
1689 # valid CWD won't fail after we check out the merge branch.
1690 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1691 if rel_base_path:
1692 os.chdir(rel_base_path)
1693
1694 # Stuff our change into the merge branch.
1695 # We wrap in a try...finally block so if anything goes wrong,
1696 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001697 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001698 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001699 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1700 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001702 RunGit(
1703 [
1704 'commit', '--author', options.contributor,
1705 '-m', commit_desc.description,
1706 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001708 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001709 if base_has_submodules:
1710 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1711 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1712 RunGit(['checkout', CHERRY_PICK_BRANCH])
1713 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001714 if cmd == 'push':
1715 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001716 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717 retcode, output = RunGitWithCode(
1718 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1719 logging.debug(output)
1720 else:
1721 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001722 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001723 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001724 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 finally:
1726 # And then swap back to the original branch and clean up.
1727 RunGit(['checkout', '-q', cl.GetBranch()])
1728 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001729 if base_has_submodules:
1730 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731
1732 if cl.GetIssue():
1733 if cmd == 'dcommit' and 'Committed r' in output:
1734 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1735 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001736 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1737 for l in output.splitlines(False))
1738 match = filter(None, match)
1739 if len(match) != 1:
1740 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1741 output)
1742 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 else:
1744 return 1
1745 viewvc_url = settings.GetViewVCUrl()
1746 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001747 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001748 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001749 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750 print ('Closing issue '
1751 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001752 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001754 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001755 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001756 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001757 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1758 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001759 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001760
1761 if retcode == 0:
1762 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1763 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001764 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766 return 0
1767
1768
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001769@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001770def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001771 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001773 message = """This doesn't appear to be an SVN repository.
1774If your project has a git mirror with an upstream SVN master, you probably need
1775to run 'git svn init', see your project's git mirror documentation.
1776If your project has a true writeable upstream repository, you probably want
1777to run 'git cl push' instead.
1778Choose wisely, if you get this wrong, your commit might appear to succeed but
1779will instead be silently ignored."""
1780 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001781 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782 return SendUpstream(parser, args, 'dcommit')
1783
1784
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001785@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001787 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788 if settings.GetIsGitSvn():
1789 print('This appears to be an SVN repository.')
1790 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001791 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001792 return SendUpstream(parser, args, 'push')
1793
1794
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001795@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796def CMDpatch(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001797 """Patchs in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001798 parser.add_option('-b', dest='newbranch',
1799 help='create a new branch off trunk for the patch')
1800 parser.add_option('-f', action='store_true', dest='force',
1801 help='with -b, clobber any existing branch')
1802 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001803 help='failed patches spew .rej files rather than '
1804 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1806 help="don't commit after patch applies")
1807 (options, args) = parser.parse_args(args)
1808 if len(args) != 1:
1809 parser.print_help()
1810 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001811 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001813 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001814 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001815
maruel@chromium.org52424302012-08-29 15:14:30 +00001816 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001818 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001819 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001820 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001821 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001822 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001823 # Assume it's a URL to the patch. Default to https.
1824 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001825 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001826 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827 DieWithError('Must pass an issue ID or full URL for '
1828 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001829 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001830 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001831 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832
1833 if options.newbranch:
1834 if options.force:
1835 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001836 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001837 RunGit(['checkout', '-b', options.newbranch,
1838 Changelist().GetUpstreamBranch()])
1839
1840 # Switch up to the top-level directory, if necessary, in preparation for
1841 # applying the patch.
1842 top = RunGit(['rev-parse', '--show-cdup']).strip()
1843 if top:
1844 os.chdir(top)
1845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 # Git patches have a/ at the beginning of source paths. We strip that out
1847 # with a sed script rather than the -p flag to patch so we can feed either
1848 # Git or svn-style patches into the same apply command.
1849 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001850 try:
1851 patch_data = subprocess2.check_output(
1852 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1853 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854 DieWithError('Git patch mungling failed.')
1855 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001856 env = os.environ.copy()
1857 # 'cat' is a magical git string that disables pagers on all platforms.
1858 env['GIT_PAGER'] = 'cat'
1859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 # We use "git apply" to apply the patch instead of "patch" so that we can
1861 # pick up file adds.
1862 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001863 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864 if options.reject:
1865 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001866 elif IsGitVersionAtLeast('1.7.12'):
1867 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001868 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001869 subprocess2.check_call(cmd, env=env,
1870 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001871 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872 DieWithError('Failed to apply the patch')
1873
1874 # If we had an issue, commit the current state and register the issue.
1875 if not options.nocommit:
1876 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1877 cl = Changelist()
1878 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001879 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001880 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 else:
1882 print "Patch applied to index."
1883 return 0
1884
1885
1886def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001887 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001888 # Provide a wrapper for git svn rebase to help avoid accidental
1889 # git svn dcommit.
1890 # It's the only command that doesn't use parser at all since we just defer
1891 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001892 env = os.environ.copy()
1893 # 'cat' is a magical git string that disables pagers on all platforms.
1894 env['GIT_PAGER'] = 'cat'
1895
1896 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897
1898
1899def GetTreeStatus():
1900 """Fetches the tree status and returns either 'open', 'closed',
1901 'unknown' or 'unset'."""
1902 url = settings.GetTreeStatusUrl(error_ok=True)
1903 if url:
1904 status = urllib2.urlopen(url).read().lower()
1905 if status.find('closed') != -1 or status == '0':
1906 return 'closed'
1907 elif status.find('open') != -1 or status == '1':
1908 return 'open'
1909 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 return 'unset'
1911
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913def GetTreeStatusReason():
1914 """Fetches the tree status from a json url and returns the message
1915 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001916 url = settings.GetTreeStatusUrl()
1917 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 connection = urllib2.urlopen(json_url)
1919 status = json.loads(connection.read())
1920 connection.close()
1921 return status['message']
1922
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001925 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001926 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 status = GetTreeStatus()
1928 if 'unset' == status:
1929 print 'You must configure your tree status URL by running "git cl config".'
1930 return 2
1931
1932 print "The tree is %s" % status
1933 print
1934 print GetTreeStatusReason()
1935 if status != 'open':
1936 return 1
1937 return 0
1938
1939
maruel@chromium.org15192402012-09-06 12:38:29 +00001940def CMDtry(parser, args):
1941 """Triggers a try job through Rietveld."""
1942 group = optparse.OptionGroup(parser, "Try job options")
1943 group.add_option(
1944 "-b", "--bot", action="append",
1945 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1946 "times to specify multiple builders. ex: "
1947 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1948 "the try server waterfall for the builders name and the tests "
1949 "available. Can also be used to specify gtest_filter, e.g. "
1950 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1951 group.add_option(
1952 "-r", "--revision",
1953 help="Revision to use for the try job; default: the "
1954 "revision will be determined by the try server; see "
1955 "its waterfall for more info")
1956 group.add_option(
1957 "-c", "--clobber", action="store_true", default=False,
1958 help="Force a clobber before building; e.g. don't do an "
1959 "incremental build")
1960 group.add_option(
1961 "--project",
1962 help="Override which project to use. Projects are defined "
1963 "server-side to define what default bot set to use")
1964 group.add_option(
1965 "-t", "--testfilter", action="append", default=[],
1966 help=("Apply a testfilter to all the selected builders. Unless the "
1967 "builders configurations are similar, use multiple "
1968 "--bot <builder>:<test> arguments."))
1969 group.add_option(
1970 "-n", "--name", help="Try job name; default to current branch name")
1971 parser.add_option_group(group)
1972 options, args = parser.parse_args(args)
1973
1974 if args:
1975 parser.error('Unknown arguments: %s' % args)
1976
1977 cl = Changelist()
1978 if not cl.GetIssue():
1979 parser.error('Need to upload first')
1980
1981 if not options.name:
1982 options.name = cl.GetBranch()
1983
1984 # Process --bot and --testfilter.
1985 if not options.bot:
1986 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001987 change = cl.GetChange(
1988 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1989 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001990 options.bot = presubmit_support.DoGetTrySlaves(
1991 change,
1992 change.LocalPaths(),
1993 settings.GetRoot(),
1994 None,
1995 None,
1996 options.verbose,
1997 sys.stdout)
1998 if not options.bot:
1999 parser.error('No default try builder to try, use --bot')
2000
2001 builders_and_tests = {}
2002 for bot in options.bot:
2003 if ':' in bot:
2004 builder, tests = bot.split(':', 1)
2005 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2006 elif ',' in bot:
2007 parser.error('Specify one bot per --bot flag')
2008 else:
2009 builders_and_tests.setdefault(bot, []).append('defaulttests')
2010
2011 if options.testfilter:
2012 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2013 builders_and_tests = dict(
2014 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2015 if t != ['compile'])
2016
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002017 if any('triggered' in b for b in builders_and_tests):
2018 print >> sys.stderr, (
2019 'ERROR You are trying to send a job to a triggered bot. This type of'
2020 ' bot requires an\ninitial job from a parent (usually a builder). '
2021 'Instead send your job to the parent.\n'
2022 'Bot list: %s' % builders_and_tests)
2023 return 1
2024
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002025 patchset = cl.GetMostRecentPatchset()
2026 if patchset and patchset != cl.GetPatchset():
2027 print(
2028 '\nWARNING Mismatch between local config and server. Did a previous '
2029 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2030 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002031
2032 cl.RpcServer().trigger_try_jobs(
2033 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2034 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002035 print('Tried jobs on:')
2036 length = max(len(builder) for builder in builders_and_tests)
2037 for builder in sorted(builders_and_tests):
2038 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002039 return 0
2040
2041
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002042@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002044 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002045 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002046 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002047 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002048 return 0
2049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002051 if args:
2052 # One arg means set upstream branch.
2053 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2054 cl = Changelist()
2055 print "Upstream branch set to " + cl.GetUpstreamBranch()
2056 else:
2057 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058 return 0
2059
2060
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002061def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002062 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002063 _, args = parser.parse_args(args)
2064 if args:
2065 parser.error('Unrecognized args: %s' % ' '.join(args))
2066 cl = Changelist()
2067 cl.SetFlag('commit', '1')
2068 return 0
2069
2070
groby@chromium.org411034a2013-02-26 15:12:01 +00002071def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002072 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002073 _, args = parser.parse_args(args)
2074 if args:
2075 parser.error('Unrecognized args: %s' % ' '.join(args))
2076 cl = Changelist()
2077 # Ensure there actually is an issue to close.
2078 cl.GetDescription()
2079 cl.CloseIssue()
2080 return 0
2081
2082
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002083def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002084 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002085 CLANG_EXTS = ['.cc', '.cpp', '.h']
2086 parser.add_option('--full', action='store_true', default=False)
2087 opts, args = parser.parse_args(args)
2088 if args:
2089 parser.error('Unrecognized args: %s' % ' '.join(args))
2090
digit@chromium.org29e47272013-05-17 17:01:46 +00002091 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002092 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002093 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002094 # Only list the names of modified files.
2095 diff_cmd.append('--name-only')
2096 else:
2097 # Only generate context-less patches.
2098 diff_cmd.append('-U0')
2099
2100 # Grab the merge-base commit, i.e. the upstream commit of the current
2101 # branch when it was created or the last time it was rebased. This is
2102 # to cover the case where the user may have called "git fetch origin",
2103 # moving the origin branch to a newer commit, but hasn't rebased yet.
2104 upstream_commit = None
2105 cl = Changelist()
2106 upstream_branch = cl.GetUpstreamBranch()
2107 if upstream_branch:
2108 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2109 upstream_commit = upstream_commit.strip()
2110
2111 if not upstream_commit:
2112 DieWithError('Could not find base commit for this branch. '
2113 'Are you in detached state?')
2114
2115 diff_cmd.append(upstream_commit)
2116
2117 # Handle source file filtering.
2118 diff_cmd.append('--')
2119 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2120 diff_output = RunGit(diff_cmd)
2121
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002122 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2123
digit@chromium.org29e47272013-05-17 17:01:46 +00002124 if opts.full:
2125 # diff_output is a list of files to send to clang-format.
2126 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002127 if not files:
2128 print "Nothing to format."
2129 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002130 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2131 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002132 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002133 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002134 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2135 'clang-format-diff.py')
2136 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002137 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2138 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002139 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002140
2141 return 0
2142
2143
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002144class OptionParser(optparse.OptionParser):
2145 """Creates the option parse and add --verbose support."""
2146 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002147 optparse.OptionParser.__init__(
2148 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002149 self.add_option(
2150 '-v', '--verbose', action='count', default=0,
2151 help='Use 2 times for more debugging info')
2152
2153 def parse_args(self, args=None, values=None):
2154 options, args = optparse.OptionParser.parse_args(self, args, values)
2155 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2156 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2157 return options, args
2158
2159 def format_description(self, _):
2160 """Disables automatic reformatting."""
2161 lines = self.description.rstrip().splitlines()
2162 lines_fixed = [lines[0]] + [l[2:] if len(l) >= 2 else l for l in lines[1:]]
2163 description = ''.join(l + '\n' for l in lines_fixed)
2164 return description[0].upper() + description[1:]
2165
2166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002167def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002168 if sys.hexversion < 0x02060000:
2169 print >> sys.stderr, (
2170 '\nYour python version %s is unsupported, please upgrade.\n' %
2171 sys.version.split(' ', 1)[0])
2172 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002173
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002174 # Reload settings.
2175 global settings
2176 settings = Settings()
2177
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002178 dispatcher = subcommand.CommandDispatcher(__name__)
2179 try:
2180 return dispatcher.execute(OptionParser(), argv)
2181 except urllib2.HTTPError, e:
2182 if e.code != 500:
2183 raise
2184 DieWithError(
2185 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2186 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187
2188
2189if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002190 # These affect sys.stdout so do it outside of main() to simplify mocks in
2191 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002192 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002193 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002194 sys.exit(main(sys.argv[1:]))