blob: dc351a2123a9f1bb29c2c7cf95806e7b4dcf7d1e [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)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001080 if args:
1081 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001084 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 if options.field.startswith('desc'):
1086 print cl.GetDescription()
1087 elif options.field == 'id':
1088 issueid = cl.GetIssue()
1089 if issueid:
1090 print issueid
1091 elif options.field == 'patch':
1092 patchset = cl.GetPatchset()
1093 if patchset:
1094 print patchset
1095 elif options.field == 'url':
1096 url = cl.GetIssueURL()
1097 if url:
1098 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001099 return 0
1100
1101 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1102 if not branches:
1103 print('No local branch found.')
1104 return 0
1105
1106 changes = (Changelist(branchref=b) for b in branches.splitlines())
1107 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1108 alignment = max(5, max(len(b) for b in branches))
1109 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001110 # Adhoc thread pool to request data concurrently.
1111 output = Queue.Queue()
1112
1113 # Silence upload.py otherwise it becomes unweldly.
1114 upload.verbosity = 0
1115
1116 if not options.fast:
1117 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001118 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001119 c = Changelist(branchref=b)
1120 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001121 props = {}
1122 r = None
1123 if i:
1124 try:
1125 props = c.GetIssueProperties()
1126 r = c.GetApprovingReviewers() if i else None
1127 except urllib2.HTTPError:
1128 # The issue probably doesn't exist anymore.
1129 i += ' (broken)'
1130
1131 msgs = props.get('messages') or []
1132
1133 if not i:
1134 color = Fore.WHITE
1135 elif props.get('closed'):
1136 # Issue is closed.
1137 color = Fore.CYAN
1138 elif r:
1139 # Was LGTM'ed.
1140 color = Fore.GREEN
1141 elif not msgs:
1142 # No message was sent.
1143 color = Fore.RED
1144 elif msgs[-1]['sender'] != props.get('owner_email'):
1145 color = Fore.YELLOW
1146 else:
1147 color = Fore.BLUE
1148 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001149
1150 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1151 for t in threads:
1152 t.daemon = True
1153 t.start()
1154 else:
1155 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1156 for b in branches:
1157 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001158 url = c.GetIssueURL()
1159 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160
1161 tmp = {}
1162 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001163 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001164 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001165 b, i, color = output.get()
1166 tmp[b] = (i, color)
1167 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001168 reset = Fore.RESET
1169 if not sys.stdout.isatty():
1170 color = ''
1171 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001172 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001173 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001174
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001175 cl = Changelist()
1176 print
1177 print 'Current branch:',
1178 if not cl.GetIssue():
1179 print 'no issue assigned.'
1180 return 0
1181 print cl.GetBranch()
1182 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1183 print 'Issue description:'
1184 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 return 0
1186
1187
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001188def colorize_CMDstatus_doc():
1189 """To be called once in main() to add colors to git cl status help."""
1190 colors = [i for i in dir(Fore) if i[0].isupper()]
1191
1192 def colorize_line(line):
1193 for color in colors:
1194 if color in line.upper():
1195 # Extract whitespaces first and the leading '-'.
1196 indent = len(line) - len(line.lstrip(' ')) + 1
1197 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1198 return line
1199
1200 lines = CMDstatus.__doc__.splitlines()
1201 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1202
1203
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001204@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001206 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207
1208 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001209 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001210 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211
1212 cl = Changelist()
1213 if len(args) > 0:
1214 try:
1215 issue = int(args[0])
1216 except ValueError:
1217 DieWithError('Pass a number to set the issue or none to list it.\n'
1218 'Maybe you want to run git cl status?')
1219 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001220 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 return 0
1222
1223
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001224def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001225 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001226 (_, args) = parser.parse_args(args)
1227 if args:
1228 parser.error('Unsupported argument: %s' % args)
1229
1230 cl = Changelist()
1231 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001233 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001234 if message['disapproval']:
1235 color = Fore.RED
1236 elif message['approval']:
1237 color = Fore.GREEN
1238 elif message['sender'] == data['owner_email']:
1239 color = Fore.MAGENTA
1240 else:
1241 color = Fore.BLUE
1242 print '\n%s%s %s%s' % (
1243 color, message['date'].split('.', 1)[0], message['sender'],
1244 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001245 if message['text'].strip():
1246 print '\n'.join(' ' + l for l in message['text'].splitlines())
1247 return 0
1248
1249
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001250def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001251 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001252 cl = Changelist()
1253 if not cl.GetIssue():
1254 DieWithError('This branch has no associated changelist.')
1255 description = ChangeDescription(cl.GetDescription())
1256 description.prompt()
1257 cl.UpdateDescription(description.description)
1258 return 0
1259
1260
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261def CreateDescriptionFromLog(args):
1262 """Pulls out the commit log to use as a base for the CL description."""
1263 log_args = []
1264 if len(args) == 1 and not args[0].endswith('.'):
1265 log_args = [args[0] + '..']
1266 elif len(args) == 1 and args[0].endswith('...'):
1267 log_args = [args[0][:-1]]
1268 elif len(args) == 2:
1269 log_args = [args[0] + '..' + args[1]]
1270 else:
1271 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001272 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273
1274
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001276 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001277 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001279 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001280 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281 (options, args) = parser.parse_args(args)
1282
ukai@chromium.org259e4682012-10-25 07:36:33 +00001283 if not options.force and is_dirty_git_tree('presubmit'):
1284 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 return 1
1286
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001287 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 if args:
1289 base_branch = args[0]
1290 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001291 # Default to diffing against the common ancestor of the upstream branch.
1292 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001294 cl.RunHook(
1295 committing=not options.upload,
1296 may_prompt=False,
1297 verbose=options.verbose,
1298 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001299 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300
1301
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001302def AddChangeIdToCommitMessage(options, args):
1303 """Re-commits using the current message, assumes the commit hook is in
1304 place.
1305 """
1306 log_desc = options.message or CreateDescriptionFromLog(args)
1307 git_command = ['commit', '--amend', '-m', log_desc]
1308 RunGit(git_command)
1309 new_log_desc = CreateDescriptionFromLog(args)
1310 if CHANGE_ID in new_log_desc:
1311 print 'git-cl: Added Change-Id to commit message.'
1312 else:
1313 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1314
1315
ukai@chromium.orge8077812012-02-03 03:41:46 +00001316def GerritUpload(options, args, cl):
1317 """upload the current branch to gerrit."""
1318 # We assume the remote called "origin" is the one we want.
1319 # It is probably not worthwhile to support different workflows.
1320 remote = 'origin'
1321 branch = 'master'
1322 if options.target_branch:
1323 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001325 change_desc = ChangeDescription(
1326 options.message or CreateDescriptionFromLog(args))
1327 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001328 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001330 if CHANGE_ID not in change_desc.description:
1331 AddChangeIdToCommitMessage(options, args)
1332 if options.reviewers:
1333 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334
ukai@chromium.orge8077812012-02-03 03:41:46 +00001335 receive_options = []
1336 cc = cl.GetCCList().split(',')
1337 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001338 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001339 cc = filter(None, cc)
1340 if cc:
1341 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001342 if change_desc.get_reviewers():
1343 receive_options.extend(
1344 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001345
ukai@chromium.orge8077812012-02-03 03:41:46 +00001346 git_command = ['push']
1347 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001348 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001349 ' '.join(receive_options))
1350 git_command += [remote, 'HEAD:refs/for/' + branch]
1351 RunGit(git_command)
1352 # TODO(ukai): parse Change-Id: and set issue number?
1353 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355
ukai@chromium.orge8077812012-02-03 03:41:46 +00001356def RietveldUpload(options, args, cl):
1357 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1359 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 if options.emulate_svn_auto_props:
1361 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
1363 change_desc = None
1364
1365 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001366 if options.title:
1367 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001368 if options.message:
1369 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001370 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 print ("This branch is associated with issue %s. "
1372 "Adding patch to that issue." % cl.GetIssue())
1373 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001374 if options.title:
1375 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001376 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001377 change_desc = ChangeDescription(message)
1378 if options.reviewers:
1379 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001380 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001381 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001382
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001383 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384 print "Description is empty; aborting."
1385 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001386
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001387 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001388 if change_desc.get_reviewers():
1389 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001390 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001391 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001392 DieWithError("Must specify reviewers to send email.")
1393 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001394 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001395 if cc:
1396 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001398 if options.private or settings.GetDefaultPrivateFlag() == "True":
1399 upload_args.append('--private')
1400
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001401 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001402 if not options.find_copies:
1403 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 # Include the upstream repo's URL in the change -- this is useful for
1406 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001407 remote_url = cl.GetGitBaseUrlFromConfig()
1408 if not remote_url:
1409 if settings.GetIsGitSvn():
1410 # URL is dependent on the current directory.
1411 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1412 if data:
1413 keys = dict(line.split(': ', 1) for line in data.splitlines()
1414 if ': ' in line)
1415 remote_url = keys.get('URL', None)
1416 else:
1417 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1418 remote_url = (cl.GetRemoteUrl() + '@'
1419 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 if remote_url:
1421 upload_args.extend(['--base_url', remote_url])
1422
1423 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001424 upload_args = ['upload'] + upload_args + args
1425 logging.info('upload.RealMain(%s)', upload_args)
1426 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001427 issue = int(issue)
1428 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001429 except KeyboardInterrupt:
1430 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 except:
1432 # If we got an exception after the user typed a description for their
1433 # change, back up the description before re-raising.
1434 if change_desc:
1435 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1436 print '\nGot exception while uploading -- saving description to %s\n' \
1437 % backup_path
1438 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001439 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 backup_file.close()
1441 raise
1442
1443 if not cl.GetIssue():
1444 cl.SetIssue(issue)
1445 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001446
1447 if options.use_commit_queue:
1448 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449 return 0
1450
1451
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001452def cleanup_list(l):
1453 """Fixes a list so that comma separated items are put as individual items.
1454
1455 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1456 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1457 """
1458 items = sum((i.split(',') for i in l), [])
1459 stripped_items = (i.strip() for i in items)
1460 return sorted(filter(None, stripped_items))
1461
1462
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001463@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001464def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001465 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001466 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1467 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001468 parser.add_option('--bypass-watchlists', action='store_true',
1469 dest='bypass_watchlists',
1470 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001471 parser.add_option('-f', action='store_true', dest='force',
1472 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001473 parser.add_option('-m', dest='message', help='message for patchset')
1474 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001475 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001476 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001477 help='reviewer email addresses')
1478 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001479 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001480 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001481 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001482 help='send email to reviewer immediately')
1483 parser.add_option("--emulate_svn_auto_props", action="store_true",
1484 dest="emulate_svn_auto_props",
1485 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001486 parser.add_option('-c', '--use-commit-queue', action='store_true',
1487 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001488 parser.add_option('--private', action='store_true',
1489 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001490 parser.add_option('--target_branch',
1491 help='When uploading to gerrit, remote branch to '
1492 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001493 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001494 (options, args) = parser.parse_args(args)
1495
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001496 if options.target_branch and not settings.GetIsGerrit():
1497 parser.error('Use --target_branch for non gerrit repository.')
1498
ukai@chromium.org259e4682012-10-25 07:36:33 +00001499 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001500 return 1
1501
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001502 options.reviewers = cleanup_list(options.reviewers)
1503 options.cc = cleanup_list(options.cc)
1504
ukai@chromium.orge8077812012-02-03 03:41:46 +00001505 cl = Changelist()
1506 if args:
1507 # TODO(ukai): is it ok for gerrit case?
1508 base_branch = args[0]
1509 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001510 # Default to diffing against common ancestor of upstream branch
1511 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001512 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001513
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001514 # Apply watchlists on upload.
1515 change = cl.GetChange(base_branch, None)
1516 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1517 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001518 if not options.bypass_watchlists:
1519 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001520
ukai@chromium.orge8077812012-02-03 03:41:46 +00001521 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001522 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001523 may_prompt=not options.force,
1524 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001525 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001526 if not hook_results.should_continue():
1527 return 1
1528 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001529 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001530
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001531 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001532 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001533 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001534 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001535 print ('The last upload made from this repository was patchset #%d but '
1536 'the most recent patchset on the server is #%d.'
1537 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001538 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1539 'from another machine or branch the patch you\'re uploading now '
1540 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001541 ask_for_data('About to upload; enter to confirm.')
1542
iannucci@chromium.org79540052012-10-19 23:15:26 +00001543 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001544 if settings.GetIsGerrit():
1545 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001546 ret = RietveldUpload(options, args, cl)
1547 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001548 git_set_branch_value('last-upload-hash',
1549 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001550
1551 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001552
1553
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001554def IsSubmoduleMergeCommit(ref):
1555 # When submodules are added to the repo, we expect there to be a single
1556 # non-git-svn merge commit at remote HEAD with a signature comment.
1557 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001558 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001559 return RunGit(cmd) != ''
1560
1561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562def SendUpstream(parser, args, cmd):
1563 """Common code for CmdPush and CmdDCommit
1564
1565 Squashed commit into a single.
1566 Updates changelog with metadata (e.g. pointer to review).
1567 Pushes/dcommits the code upstream.
1568 Updates review and closes.
1569 """
1570 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1571 help='bypass upload presubmit hook')
1572 parser.add_option('-m', dest='message',
1573 help="override review description")
1574 parser.add_option('-f', action='store_true', dest='force',
1575 help="force yes to questions (don't prompt)")
1576 parser.add_option('-c', dest='contributor',
1577 help="external contributor for patch (appended to " +
1578 "description and used as author for git). Should be " +
1579 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001580 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001581 (options, args) = parser.parse_args(args)
1582 cl = Changelist()
1583
1584 if not args or cmd == 'push':
1585 # Default to merging against our best guess of the upstream branch.
1586 args = [cl.GetUpstreamBranch()]
1587
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001588 if options.contributor:
1589 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1590 print "Please provide contibutor as 'First Last <email@example.com>'"
1591 return 1
1592
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001594 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001595
ukai@chromium.org259e4682012-10-25 07:36:33 +00001596 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 return 1
1598
1599 # This rev-list syntax means "show all commits not in my branch that
1600 # are in base_branch".
1601 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1602 base_branch]).splitlines()
1603 if upstream_commits:
1604 print ('Base branch "%s" has %d commits '
1605 'not in this branch.' % (base_branch, len(upstream_commits)))
1606 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1607 return 1
1608
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001609 # This is the revision `svn dcommit` will commit on top of.
1610 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1611 '--pretty=format:%H'])
1612
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001614 # If the base_head is a submodule merge commit, the first parent of the
1615 # base_head should be a git-svn commit, which is what we're interested in.
1616 base_svn_head = base_branch
1617 if base_has_submodules:
1618 base_svn_head += '^1'
1619
1620 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621 if extra_commits:
1622 print ('This branch has %d additional commits not upstreamed yet.'
1623 % len(extra_commits.splitlines()))
1624 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1625 'before attempting to %s.' % (base_branch, cmd))
1626 return 1
1627
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001628 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001629 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001630 author = None
1631 if options.contributor:
1632 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001633 hook_results = cl.RunHook(
1634 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001635 may_prompt=not options.force,
1636 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001637 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001638 if not hook_results.should_continue():
1639 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640
1641 if cmd == 'dcommit':
1642 # Check the tree status if the tree status URL is set.
1643 status = GetTreeStatus()
1644 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001645 print('The tree is closed. Please wait for it to reopen. Use '
1646 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647 return 1
1648 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001649 print('Unable to determine tree status. Please verify manually and '
1650 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001651 else:
1652 breakpad.SendStack(
1653 'GitClHooksBypassedCommit',
1654 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001655 (cl.GetRietveldServer(), cl.GetIssue()),
1656 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001657
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001658 change_desc = ChangeDescription(options.message)
1659 if not change_desc.description and cl.GetIssue():
1660 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001661
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001662 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001663 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001664 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001665 else:
1666 print 'No description set.'
1667 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1668 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001669
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001670 # Keep a separate copy for the commit message, because the commit message
1671 # contains the link to the Rietveld issue, while the Rietveld message contains
1672 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001673 # Keep a separate copy for the commit message.
1674 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001675 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001676
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001677 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001678 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001679 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001681 commit_desc.append_footer('Patch from %s.' % options.contributor)
1682
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001683 print('Description:')
1684 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001685
1686 branches = [base_branch, cl.GetBranchRef()]
1687 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001688 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001689 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001691 # We want to squash all this branch's commits into one commit with the proper
1692 # description. We do this by doing a "reset --soft" to the base branch (which
1693 # keeps the working copy the same), then dcommitting that. If origin/master
1694 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1695 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001696 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001697 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1698 # Delete the branches if they exist.
1699 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1700 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1701 result = RunGitWithCode(showref_cmd)
1702 if result[0] == 0:
1703 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704
1705 # We might be in a directory that's present in this branch but not in the
1706 # trunk. Move up to the top of the tree so that git commands that expect a
1707 # valid CWD won't fail after we check out the merge branch.
1708 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1709 if rel_base_path:
1710 os.chdir(rel_base_path)
1711
1712 # Stuff our change into the merge branch.
1713 # We wrap in a try...finally block so if anything goes wrong,
1714 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001715 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001717 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1718 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001719 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001720 RunGit(
1721 [
1722 'commit', '--author', options.contributor,
1723 '-m', commit_desc.description,
1724 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001726 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001727 if base_has_submodules:
1728 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1729 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1730 RunGit(['checkout', CHERRY_PICK_BRANCH])
1731 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732 if cmd == 'push':
1733 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001734 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 retcode, output = RunGitWithCode(
1736 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1737 logging.debug(output)
1738 else:
1739 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001740 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001741 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001742 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 finally:
1744 # And then swap back to the original branch and clean up.
1745 RunGit(['checkout', '-q', cl.GetBranch()])
1746 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001747 if base_has_submodules:
1748 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749
1750 if cl.GetIssue():
1751 if cmd == 'dcommit' and 'Committed r' in output:
1752 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1753 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001754 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1755 for l in output.splitlines(False))
1756 match = filter(None, match)
1757 if len(match) != 1:
1758 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1759 output)
1760 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001761 else:
1762 return 1
1763 viewvc_url = settings.GetViewVCUrl()
1764 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001765 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001766 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001767 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 print ('Closing issue '
1769 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001770 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001772 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001773 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001774 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001775 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1776 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001777 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001778
1779 if retcode == 0:
1780 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1781 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001782 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784 return 0
1785
1786
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001787@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001789 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001791 message = """This doesn't appear to be an SVN repository.
1792If your project has a git mirror with an upstream SVN master, you probably need
1793to run 'git svn init', see your project's git mirror documentation.
1794If your project has a true writeable upstream repository, you probably want
1795to run 'git cl push' instead.
1796Choose wisely, if you get this wrong, your commit might appear to succeed but
1797will instead be silently ignored."""
1798 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001799 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800 return SendUpstream(parser, args, 'dcommit')
1801
1802
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001803@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001804def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001805 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 if settings.GetIsGitSvn():
1807 print('This appears to be an SVN repository.')
1808 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001809 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810 return SendUpstream(parser, args, 'push')
1811
1812
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001813@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814def CMDpatch(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001815 """Patchs in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001816 parser.add_option('-b', dest='newbranch',
1817 help='create a new branch off trunk for the patch')
1818 parser.add_option('-f', action='store_true', dest='force',
1819 help='with -b, clobber any existing branch')
1820 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001821 help='failed patches spew .rej files rather than '
1822 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001823 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1824 help="don't commit after patch applies")
1825 (options, args) = parser.parse_args(args)
1826 if len(args) != 1:
1827 parser.print_help()
1828 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001829 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001831 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001832 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001833
maruel@chromium.org52424302012-08-29 15:14:30 +00001834 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001836 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001837 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001838 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001839 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001841 # Assume it's a URL to the patch. Default to https.
1842 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001843 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001844 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 DieWithError('Must pass an issue ID or full URL for '
1846 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001847 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001848 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001849 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001850
1851 if options.newbranch:
1852 if options.force:
1853 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001854 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855 RunGit(['checkout', '-b', options.newbranch,
1856 Changelist().GetUpstreamBranch()])
1857
1858 # Switch up to the top-level directory, if necessary, in preparation for
1859 # applying the patch.
1860 top = RunGit(['rev-parse', '--show-cdup']).strip()
1861 if top:
1862 os.chdir(top)
1863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864 # Git patches have a/ at the beginning of source paths. We strip that out
1865 # with a sed script rather than the -p flag to patch so we can feed either
1866 # Git or svn-style patches into the same apply command.
1867 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001868 try:
1869 patch_data = subprocess2.check_output(
1870 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1871 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872 DieWithError('Git patch mungling failed.')
1873 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001874 env = os.environ.copy()
1875 # 'cat' is a magical git string that disables pagers on all platforms.
1876 env['GIT_PAGER'] = 'cat'
1877
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001878 # We use "git apply" to apply the patch instead of "patch" so that we can
1879 # pick up file adds.
1880 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001881 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001882 if options.reject:
1883 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001884 elif IsGitVersionAtLeast('1.7.12'):
1885 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001886 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001887 subprocess2.check_call(cmd, env=env,
1888 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001889 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001890 DieWithError('Failed to apply the patch')
1891
1892 # If we had an issue, commit the current state and register the issue.
1893 if not options.nocommit:
1894 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1895 cl = Changelist()
1896 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001897 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001898 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001899 else:
1900 print "Patch applied to index."
1901 return 0
1902
1903
1904def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001905 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906 # Provide a wrapper for git svn rebase to help avoid accidental
1907 # git svn dcommit.
1908 # It's the only command that doesn't use parser at all since we just defer
1909 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001910 env = os.environ.copy()
1911 # 'cat' is a magical git string that disables pagers on all platforms.
1912 env['GIT_PAGER'] = 'cat'
1913
1914 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915
1916
1917def GetTreeStatus():
1918 """Fetches the tree status and returns either 'open', 'closed',
1919 'unknown' or 'unset'."""
1920 url = settings.GetTreeStatusUrl(error_ok=True)
1921 if url:
1922 status = urllib2.urlopen(url).read().lower()
1923 if status.find('closed') != -1 or status == '0':
1924 return 'closed'
1925 elif status.find('open') != -1 or status == '1':
1926 return 'open'
1927 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928 return 'unset'
1929
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001930
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931def GetTreeStatusReason():
1932 """Fetches the tree status from a json url and returns the message
1933 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001934 url = settings.GetTreeStatusUrl()
1935 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936 connection = urllib2.urlopen(json_url)
1937 status = json.loads(connection.read())
1938 connection.close()
1939 return status['message']
1940
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001941
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001943 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001944 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945 status = GetTreeStatus()
1946 if 'unset' == status:
1947 print 'You must configure your tree status URL by running "git cl config".'
1948 return 2
1949
1950 print "The tree is %s" % status
1951 print
1952 print GetTreeStatusReason()
1953 if status != 'open':
1954 return 1
1955 return 0
1956
1957
maruel@chromium.org15192402012-09-06 12:38:29 +00001958def CMDtry(parser, args):
1959 """Triggers a try job through Rietveld."""
1960 group = optparse.OptionGroup(parser, "Try job options")
1961 group.add_option(
1962 "-b", "--bot", action="append",
1963 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1964 "times to specify multiple builders. ex: "
1965 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1966 "the try server waterfall for the builders name and the tests "
1967 "available. Can also be used to specify gtest_filter, e.g. "
1968 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1969 group.add_option(
1970 "-r", "--revision",
1971 help="Revision to use for the try job; default: the "
1972 "revision will be determined by the try server; see "
1973 "its waterfall for more info")
1974 group.add_option(
1975 "-c", "--clobber", action="store_true", default=False,
1976 help="Force a clobber before building; e.g. don't do an "
1977 "incremental build")
1978 group.add_option(
1979 "--project",
1980 help="Override which project to use. Projects are defined "
1981 "server-side to define what default bot set to use")
1982 group.add_option(
1983 "-t", "--testfilter", action="append", default=[],
1984 help=("Apply a testfilter to all the selected builders. Unless the "
1985 "builders configurations are similar, use multiple "
1986 "--bot <builder>:<test> arguments."))
1987 group.add_option(
1988 "-n", "--name", help="Try job name; default to current branch name")
1989 parser.add_option_group(group)
1990 options, args = parser.parse_args(args)
1991
1992 if args:
1993 parser.error('Unknown arguments: %s' % args)
1994
1995 cl = Changelist()
1996 if not cl.GetIssue():
1997 parser.error('Need to upload first')
1998
1999 if not options.name:
2000 options.name = cl.GetBranch()
2001
2002 # Process --bot and --testfilter.
2003 if not options.bot:
2004 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002005 change = cl.GetChange(
2006 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2007 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002008 options.bot = presubmit_support.DoGetTrySlaves(
2009 change,
2010 change.LocalPaths(),
2011 settings.GetRoot(),
2012 None,
2013 None,
2014 options.verbose,
2015 sys.stdout)
2016 if not options.bot:
2017 parser.error('No default try builder to try, use --bot')
2018
2019 builders_and_tests = {}
2020 for bot in options.bot:
2021 if ':' in bot:
2022 builder, tests = bot.split(':', 1)
2023 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2024 elif ',' in bot:
2025 parser.error('Specify one bot per --bot flag')
2026 else:
2027 builders_and_tests.setdefault(bot, []).append('defaulttests')
2028
2029 if options.testfilter:
2030 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2031 builders_and_tests = dict(
2032 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2033 if t != ['compile'])
2034
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002035 if any('triggered' in b for b in builders_and_tests):
2036 print >> sys.stderr, (
2037 'ERROR You are trying to send a job to a triggered bot. This type of'
2038 ' bot requires an\ninitial job from a parent (usually a builder). '
2039 'Instead send your job to the parent.\n'
2040 'Bot list: %s' % builders_and_tests)
2041 return 1
2042
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002043 patchset = cl.GetMostRecentPatchset()
2044 if patchset and patchset != cl.GetPatchset():
2045 print(
2046 '\nWARNING Mismatch between local config and server. Did a previous '
2047 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2048 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002049
2050 cl.RpcServer().trigger_try_jobs(
2051 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2052 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002053 print('Tried jobs on:')
2054 length = max(len(builder) for builder in builders_and_tests)
2055 for builder in sorted(builders_and_tests):
2056 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002057 return 0
2058
2059
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002060@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002061def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002062 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002063 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002064 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002065 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002066 return 0
2067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002069 if args:
2070 # One arg means set upstream branch.
2071 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2072 cl = Changelist()
2073 print "Upstream branch set to " + cl.GetUpstreamBranch()
2074 else:
2075 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076 return 0
2077
2078
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002079def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002080 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002081 _, args = parser.parse_args(args)
2082 if args:
2083 parser.error('Unrecognized args: %s' % ' '.join(args))
2084 cl = Changelist()
2085 cl.SetFlag('commit', '1')
2086 return 0
2087
2088
groby@chromium.org411034a2013-02-26 15:12:01 +00002089def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002090 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002091 _, args = parser.parse_args(args)
2092 if args:
2093 parser.error('Unrecognized args: %s' % ' '.join(args))
2094 cl = Changelist()
2095 # Ensure there actually is an issue to close.
2096 cl.GetDescription()
2097 cl.CloseIssue()
2098 return 0
2099
2100
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002101def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002102 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002103 CLANG_EXTS = ['.cc', '.cpp', '.h']
2104 parser.add_option('--full', action='store_true', default=False)
2105 opts, args = parser.parse_args(args)
2106 if args:
2107 parser.error('Unrecognized args: %s' % ' '.join(args))
2108
digit@chromium.org29e47272013-05-17 17:01:46 +00002109 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002110 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002111 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002112 # Only list the names of modified files.
2113 diff_cmd.append('--name-only')
2114 else:
2115 # Only generate context-less patches.
2116 diff_cmd.append('-U0')
2117
2118 # Grab the merge-base commit, i.e. the upstream commit of the current
2119 # branch when it was created or the last time it was rebased. This is
2120 # to cover the case where the user may have called "git fetch origin",
2121 # moving the origin branch to a newer commit, but hasn't rebased yet.
2122 upstream_commit = None
2123 cl = Changelist()
2124 upstream_branch = cl.GetUpstreamBranch()
2125 if upstream_branch:
2126 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2127 upstream_commit = upstream_commit.strip()
2128
2129 if not upstream_commit:
2130 DieWithError('Could not find base commit for this branch. '
2131 'Are you in detached state?')
2132
2133 diff_cmd.append(upstream_commit)
2134
2135 # Handle source file filtering.
2136 diff_cmd.append('--')
2137 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2138 diff_output = RunGit(diff_cmd)
2139
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002140 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2141
digit@chromium.org29e47272013-05-17 17:01:46 +00002142 if opts.full:
2143 # diff_output is a list of files to send to clang-format.
2144 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002145 if not files:
2146 print "Nothing to format."
2147 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002148 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2149 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002150 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002151 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002152 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2153 'clang-format-diff.py')
2154 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002155 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2156 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002157 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002158
2159 return 0
2160
2161
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002162class OptionParser(optparse.OptionParser):
2163 """Creates the option parse and add --verbose support."""
2164 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002165 optparse.OptionParser.__init__(
2166 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002167 self.add_option(
2168 '-v', '--verbose', action='count', default=0,
2169 help='Use 2 times for more debugging info')
2170
2171 def parse_args(self, args=None, values=None):
2172 options, args = optparse.OptionParser.parse_args(self, args, values)
2173 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2174 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2175 return options, args
2176
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002178def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002179 if sys.hexversion < 0x02060000:
2180 print >> sys.stderr, (
2181 '\nYour python version %s is unsupported, please upgrade.\n' %
2182 sys.version.split(' ', 1)[0])
2183 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002184
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002185 # Reload settings.
2186 global settings
2187 settings = Settings()
2188
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002189 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002190 dispatcher = subcommand.CommandDispatcher(__name__)
2191 try:
2192 return dispatcher.execute(OptionParser(), argv)
2193 except urllib2.HTTPError, e:
2194 if e.code != 500:
2195 raise
2196 DieWithError(
2197 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2198 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002199
2200
2201if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002202 # These affect sys.stdout so do it outside of main() to simplify mocks in
2203 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002204 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002205 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206 sys.exit(main(sys.argv[1:]))