blob: 7a43959726d8d4dbe6922787f07fbdbe6bede381 [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
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000027from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000028from third_party import upload
29import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000030import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000031import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000033import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000035import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import watchlists
37
38
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000039DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000040POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000042GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000043CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000044
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045# Shortcut since it quickly becomes redundant.
46Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000047
maruel@chromium.orgddd59412011-11-30 14:20:38 +000048# Initialized in main()
49settings = None
50
51
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000053 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054 sys.exit(1)
55
56
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000059 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000060 except subprocess2.CalledProcessError as e:
61 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000064 'Command "%s" failed.\n%s' % (
65 ' '.join(args), error_message or e.stdout or ''))
66 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
68
69def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 """Returns stdout."""
bratell@opera.comf267b0e2013-05-02 09:11:43 +000071 return RunCommand(['git', '--no-pager'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
73
74def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000075 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000076 try:
bratell@opera.comf267b0e2013-05-02 09:11:43 +000077 out, code = subprocess2.communicate(['git', '--no-pager'] + args,
78 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000079 return code, out[0]
80 except ValueError:
81 # When the subprocess fails, it returns None. That triggers a ValueError
82 # when trying to unpack the return value into (out, code).
83 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084
85
86def usage(more):
87 def hook(fn):
88 fn.usage_more = more
89 return fn
90 return hook
91
92
maruel@chromium.org90541732011-04-01 17:54:18 +000093def ask_for_data(prompt):
94 try:
95 return raw_input(prompt)
96 except KeyboardInterrupt:
97 # Hide the exception.
98 sys.exit(1)
99
100
iannucci@chromium.org79540052012-10-19 23:15:26 +0000101def git_set_branch_value(key, value):
102 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000103 if not branch:
104 return
105
106 cmd = ['config']
107 if isinstance(value, int):
108 cmd.append('--int')
109 git_key = 'branch.%s.%s' % (branch, key)
110 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000111
112
113def git_get_branch_default(key, default):
114 branch = Changelist().GetBranch()
115 if branch:
116 git_key = 'branch.%s.%s' % (branch, key)
117 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
118 try:
119 return int(stdout.strip())
120 except ValueError:
121 pass
122 return default
123
124
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000125def add_git_similarity(parser):
126 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000127 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000128 help='Sets the percentage that a pair of files need to match in order to'
129 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000130 parser.add_option(
131 '--find-copies', action='store_true',
132 help='Allows git to look for copies.')
133 parser.add_option(
134 '--no-find-copies', action='store_false', dest='find_copies',
135 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000136
137 old_parser_args = parser.parse_args
138 def Parse(args):
139 options, args = old_parser_args(args)
140
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000141 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000143 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000144 print('Note: Saving similarity of %d%% in git config.'
145 % options.similarity)
146 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000147
iannucci@chromium.org79540052012-10-19 23:15:26 +0000148 options.similarity = max(0, min(options.similarity, 100))
149
150 if options.find_copies is None:
151 options.find_copies = bool(
152 git_get_branch_default('git-find-copies', True))
153 else:
154 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155
156 print('Using %d%% similarity for rename/copy detection. '
157 'Override with --similarity.' % options.similarity)
158
159 return options, args
160 parser.parse_args = Parse
161
162
ukai@chromium.org259e4682012-10-25 07:36:33 +0000163def is_dirty_git_tree(cmd):
164 # Make sure index is up-to-date before running diff-index.
165 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
166 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
167 if dirty:
168 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
169 print 'Uncommitted files: (git diff-index --name-status HEAD)'
170 print dirty[:4096]
171 if len(dirty) > 4096:
172 print '... (run "git diff-index --name-status HEAD" to see full output).'
173 return True
174 return False
175
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000176
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000177def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
178 """Return the corresponding git ref if |base_url| together with |glob_spec|
179 matches the full |url|.
180
181 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
182 """
183 fetch_suburl, as_ref = glob_spec.split(':')
184 if allow_wildcards:
185 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
186 if glob_match:
187 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
188 # "branches/{472,597,648}/src:refs/remotes/svn/*".
189 branch_re = re.escape(base_url)
190 if glob_match.group(1):
191 branch_re += '/' + re.escape(glob_match.group(1))
192 wildcard = glob_match.group(2)
193 if wildcard == '*':
194 branch_re += '([^/]*)'
195 else:
196 # Escape and replace surrounding braces with parentheses and commas
197 # with pipe symbols.
198 wildcard = re.escape(wildcard)
199 wildcard = re.sub('^\\\\{', '(', wildcard)
200 wildcard = re.sub('\\\\,', '|', wildcard)
201 wildcard = re.sub('\\\\}$', ')', wildcard)
202 branch_re += wildcard
203 if glob_match.group(3):
204 branch_re += re.escape(glob_match.group(3))
205 match = re.match(branch_re, url)
206 if match:
207 return re.sub('\*$', match.group(1), as_ref)
208
209 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
210 if fetch_suburl:
211 full_url = base_url + '/' + fetch_suburl
212 else:
213 full_url = base_url
214 if full_url == url:
215 return as_ref
216 return None
217
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000218
iannucci@chromium.org79540052012-10-19 23:15:26 +0000219def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000220 """Prints statistics about the change to the user."""
221 # --no-ext-diff is broken in some versions of Git, so try to work around
222 # this by overriding the environment (but there is still a problem if the
223 # git config key "diff.external" is used).
224 env = os.environ.copy()
225 if 'GIT_EXTERNAL_DIFF' in env:
226 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000227
228 if find_copies:
229 similarity_options = ['--find-copies-harder', '-l100000',
230 '-C%s' % similarity]
231 else:
232 similarity_options = ['-M%s' % similarity]
233
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000234 return subprocess2.call(
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000235 ['git', '--no-pager',
236 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000237 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000238
239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000240class Settings(object):
241 def __init__(self):
242 self.default_server = None
243 self.cc = None
244 self.root = None
245 self.is_git_svn = None
246 self.svn_branch = None
247 self.tree_status_url = None
248 self.viewvc_url = None
249 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000250 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000251 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000252
253 def LazyUpdateIfNeeded(self):
254 """Updates the settings from a codereview.settings file, if available."""
255 if not self.updated:
256 cr_settings_file = FindCodereviewSettingsFile()
257 if cr_settings_file:
258 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000259 self.updated = True
260 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000261 self.updated = True
262
263 def GetDefaultServerUrl(self, error_ok=False):
264 if not self.default_server:
265 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000266 self.default_server = gclient_utils.UpgradeToHttps(
267 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268 if error_ok:
269 return self.default_server
270 if not self.default_server:
271 error_message = ('Could not find settings file. You must configure '
272 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000273 self.default_server = gclient_utils.UpgradeToHttps(
274 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 return self.default_server
276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 def GetRoot(self):
278 if not self.root:
279 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
280 return self.root
281
282 def GetIsGitSvn(self):
283 """Return true if this repo looks like it's using git-svn."""
284 if self.is_git_svn is None:
285 # If you have any "svn-remote.*" config keys, we think you're using svn.
286 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000287 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288 return self.is_git_svn
289
290 def GetSVNBranch(self):
291 if self.svn_branch is None:
292 if not self.GetIsGitSvn():
293 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
294
295 # Try to figure out which remote branch we're based on.
296 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000297 # 1) iterate through our branch history and find the svn URL.
298 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299
300 # regexp matching the git-svn line that contains the URL.
301 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
302
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000303 # We don't want to go through all of history, so read a line from the
304 # pipe at a time.
305 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000306 cmd = ['git', '--no-pager', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000307 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000308 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000309 for line in proc.stdout:
310 match = git_svn_re.match(line)
311 if match:
312 url = match.group(1)
313 proc.stdout.close() # Cut pipe.
314 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000315
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000316 if url:
317 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
318 remotes = RunGit(['config', '--get-regexp',
319 r'^svn-remote\..*\.url']).splitlines()
320 for remote in remotes:
321 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000323 remote = match.group(1)
324 base_url = match.group(2)
325 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000326 ['config', 'svn-remote.%s.fetch' % remote],
327 error_ok=True).strip()
328 if fetch_spec:
329 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
330 if self.svn_branch:
331 break
332 branch_spec = RunGit(
333 ['config', 'svn-remote.%s.branches' % remote],
334 error_ok=True).strip()
335 if branch_spec:
336 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
337 if self.svn_branch:
338 break
339 tag_spec = RunGit(
340 ['config', 'svn-remote.%s.tags' % remote],
341 error_ok=True).strip()
342 if tag_spec:
343 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
344 if self.svn_branch:
345 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000346
347 if not self.svn_branch:
348 DieWithError('Can\'t guess svn branch -- try specifying it on the '
349 'command line')
350
351 return self.svn_branch
352
353 def GetTreeStatusUrl(self, error_ok=False):
354 if not self.tree_status_url:
355 error_message = ('You must configure your tree status URL by running '
356 '"git cl config".')
357 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
358 error_ok=error_ok,
359 error_message=error_message)
360 return self.tree_status_url
361
362 def GetViewVCUrl(self):
363 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000364 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365 return self.viewvc_url
366
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000367 def GetDefaultCCList(self):
368 return self._GetConfig('rietveld.cc', error_ok=True)
369
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000370 def GetDefaultPrivateFlag(self):
371 return self._GetConfig('rietveld.private', error_ok=True)
372
ukai@chromium.orge8077812012-02-03 03:41:46 +0000373 def GetIsGerrit(self):
374 """Return true if this repo is assosiated with gerrit code review system."""
375 if self.is_gerrit is None:
376 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
377 return self.is_gerrit
378
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000379 def GetGitEditor(self):
380 """Return the editor specified in the git config, or None if none is."""
381 if self.git_editor is None:
382 self.git_editor = self._GetConfig('core.editor', error_ok=True)
383 return self.git_editor or None
384
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000385 def _GetConfig(self, param, **kwargs):
386 self.LazyUpdateIfNeeded()
387 return RunGit(['config', param], **kwargs).strip()
388
389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000390def ShortBranchName(branch):
391 """Convert a name like 'refs/heads/foo' to just 'foo'."""
392 return branch.replace('refs/heads/', '')
393
394
395class Changelist(object):
396 def __init__(self, branchref=None):
397 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000398 global settings
399 if not settings:
400 # Happens when git_cl.py is used as a utility library.
401 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 settings.GetDefaultServerUrl()
403 self.branchref = branchref
404 if self.branchref:
405 self.branch = ShortBranchName(self.branchref)
406 else:
407 self.branch = None
408 self.rietveld_server = None
409 self.upstream_branch = None
410 self.has_issue = False
411 self.issue = None
412 self.has_description = False
413 self.description = None
414 self.has_patchset = False
415 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000416 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000417 self.cc = None
418 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000419 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000420
421 def GetCCList(self):
422 """Return the users cc'd on this CL.
423
424 Return is a string suitable for passing to gcl with the --cc flag.
425 """
426 if self.cc is None:
427 base_cc = settings .GetDefaultCCList()
428 more_cc = ','.join(self.watchers)
429 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
430 return self.cc
431
432 def SetWatchers(self, watchers):
433 """Set the list of email addresses that should be cc'd based on the changed
434 files in this CL.
435 """
436 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437
438 def GetBranch(self):
439 """Returns the short branch name, e.g. 'master'."""
440 if not self.branch:
441 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
442 self.branch = ShortBranchName(self.branchref)
443 return self.branch
444
445 def GetBranchRef(self):
446 """Returns the full branch name, e.g. 'refs/heads/master'."""
447 self.GetBranch() # Poke the lazy loader.
448 return self.branchref
449
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000450 @staticmethod
451 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 """Returns a tuple containg remote and remote ref,
453 e.g. 'origin', 'refs/heads/master'
454 """
455 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
457 error_ok=True).strip()
458 if upstream_branch:
459 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
460 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000461 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
462 error_ok=True).strip()
463 if upstream_branch:
464 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000466 # Fall back on trying a git-svn upstream branch.
467 if settings.GetIsGitSvn():
468 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000470 # Else, try to guess the origin remote.
471 remote_branches = RunGit(['branch', '-r']).split()
472 if 'origin/master' in remote_branches:
473 # Fall back on origin/master if it exits.
474 remote = 'origin'
475 upstream_branch = 'refs/heads/master'
476 elif 'origin/trunk' in remote_branches:
477 # Fall back on origin/trunk if it exists. Generally a shared
478 # git-svn clone
479 remote = 'origin'
480 upstream_branch = 'refs/heads/trunk'
481 else:
482 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000483Either pass complete "git diff"-style arguments, like
484 git cl upload origin/master
485or verify this branch is set up to track another (via the --track argument to
486"git checkout -b ...").""")
487
488 return remote, upstream_branch
489
490 def GetUpstreamBranch(self):
491 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000492 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 if remote is not '.':
494 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
495 self.upstream_branch = upstream_branch
496 return self.upstream_branch
497
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000498 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000499 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000500 remote, branch = None, self.GetBranch()
501 seen_branches = set()
502 while branch not in seen_branches:
503 seen_branches.add(branch)
504 remote, branch = self.FetchUpstreamTuple(branch)
505 branch = ShortBranchName(branch)
506 if remote != '.' or branch.startswith('refs/remotes'):
507 break
508 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000509 remotes = RunGit(['remote'], error_ok=True).split()
510 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000511 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000512 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000513 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000514 logging.warning('Could not determine which remote this change is '
515 'associated with, so defaulting to "%s". This may '
516 'not be what you want. You may prevent this message '
517 'by running "git svn info" as documented here: %s',
518 self._remote,
519 GIT_INSTRUCTIONS_URL)
520 else:
521 logging.warn('Could not determine which remote this change is '
522 'associated with. You may prevent this message by '
523 'running "git svn info" as documented here: %s',
524 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000525 branch = 'HEAD'
526 if branch.startswith('refs/remotes'):
527 self._remote = (remote, branch)
528 else:
529 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000530 return self._remote
531
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000532 def GitSanityChecks(self, upstream_git_obj):
533 """Checks git repo status and ensures diff is from local commits."""
534
535 # Verify the commit we're diffing against is in our current branch.
536 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
537 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
538 if upstream_sha != common_ancestor:
539 print >> sys.stderr, (
540 'ERROR: %s is not in the current branch. You may need to rebase '
541 'your tracking branch' % upstream_sha)
542 return False
543
544 # List the commits inside the diff, and verify they are all local.
545 commits_in_diff = RunGit(
546 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
547 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
548 remote_branch = remote_branch.strip()
549 if code != 0:
550 _, remote_branch = self.GetRemoteBranch()
551
552 commits_in_remote = RunGit(
553 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
554
555 common_commits = set(commits_in_diff) & set(commits_in_remote)
556 if common_commits:
557 print >> sys.stderr, (
558 'ERROR: Your diff contains %d commits already in %s.\n'
559 'Run "git log --oneline %s..HEAD" to get a list of commits in '
560 'the diff. If you are using a custom git flow, you can override'
561 ' the reference used for this check with "git config '
562 'gitcl.remotebranch <git-ref>".' % (
563 len(common_commits), remote_branch, upstream_git_obj))
564 return False
565 return True
566
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000567 def GetGitBaseUrlFromConfig(self):
568 """Return the configured base URL from branch.<branchname>.baseurl.
569
570 Returns None if it is not set.
571 """
572 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
573 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 def GetRemoteUrl(self):
576 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
577
578 Returns None if there is no remote.
579 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000580 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000581 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
582
583 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000584 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
587 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000588 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589 else:
590 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591 self.has_issue = True
592 return self.issue
593
594 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000595 if not self.rietveld_server:
596 # If we're on a branch then get the server potentially associated
597 # with that branch.
598 if self.GetIssue():
599 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
600 ['config', self._RietveldServer()], error_ok=True).strip())
601 if not self.rietveld_server:
602 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 return self.rietveld_server
604
605 def GetIssueURL(self):
606 """Get the URL for a particular issue."""
607 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
608
609 def GetDescription(self, pretty=False):
610 if not self.has_description:
611 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000612 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000613 try:
614 self.description = self.RpcServer().get_description(issue).strip()
615 except urllib2.HTTPError, e:
616 if e.code == 404:
617 DieWithError(
618 ('\nWhile fetching the description for issue %d, received a '
619 '404 (not found)\n'
620 'error. It is likely that you deleted this '
621 'issue on the server. If this is the\n'
622 'case, please run\n\n'
623 ' git cl issue 0\n\n'
624 'to clear the association with the deleted issue. Then run '
625 'this command again.') % issue)
626 else:
627 DieWithError(
628 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 self.has_description = True
630 if pretty:
631 wrapper = textwrap.TextWrapper()
632 wrapper.initial_indent = wrapper.subsequent_indent = ' '
633 return wrapper.fill(self.description)
634 return self.description
635
636 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000637 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 if not self.has_patchset:
639 patchset = RunGit(['config', self._PatchsetSetting()],
640 error_ok=True).strip()
641 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000642 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 else:
644 self.patchset = None
645 self.has_patchset = True
646 return self.patchset
647
648 def SetPatchset(self, patchset):
649 """Set this branch's patchset. If patchset=0, clears the patchset."""
650 if patchset:
651 RunGit(['config', self._PatchsetSetting(), str(patchset)])
652 else:
653 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000654 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 self.has_patchset = False
656
binji@chromium.org0281f522012-09-14 13:37:59 +0000657 def GetMostRecentPatchset(self, issue):
658 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000659 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000660
661 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000662 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000663 '/download/issue%s_%s.diff' % (issue, patchset))
664
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000665 def GetApprovingReviewers(self, issue):
666 return get_approving_reviewers(
667 self.RpcServer().get_issue_properties(int(issue), True))
668
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 def SetIssue(self, issue):
670 """Set this branch's issue. If issue=0, clears the issue."""
671 if issue:
672 RunGit(['config', self._IssueSetting(), str(issue)])
673 if self.rietveld_server:
674 RunGit(['config', self._RietveldServer(), self.rietveld_server])
675 else:
676 RunGit(['config', '--unset', self._IssueSetting()])
677 self.SetPatchset(0)
678 self.has_issue = False
679
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000680 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000681 if not self.GitSanityChecks(upstream_branch):
682 DieWithError('\nGit sanity check failure')
683
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000684 root = RunCommand(['git', '--no-pager', 'rev-parse', '--show-cdup']).strip()
685 if not root:
686 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000687 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000688
689 # We use the sha1 of HEAD as a name of this change.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000690 name = RunCommand(['git', '--no-pager', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000691 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000692 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000693 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000694 except subprocess2.CalledProcessError:
695 DieWithError(
696 ('\nFailed to diff against upstream branch %s!\n\n'
697 'This branch probably doesn\'t exist anymore. To reset the\n'
698 'tracking branch, please run\n'
699 ' git branch --set-upstream %s trunk\n'
700 'replacing trunk with origin/master or the relevant branch') %
701 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000702
maruel@chromium.org52424302012-08-29 15:14:30 +0000703 issue = self.GetIssue()
704 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000705 if issue:
706 description = self.GetDescription()
707 else:
708 # If the change was never uploaded, use the log messages of all commits
709 # up to the branch point, as git cl upload will prefill the description
710 # with these log messages.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000711 description = RunCommand(['git', '--no-pager',
712 'log', '--pretty=format:%s%n%n%b',
maruel@chromium.org373af802012-05-25 21:07:33 +0000713 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000714
715 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000716 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000717 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000718 name,
719 description,
720 absroot,
721 files,
722 issue,
723 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000724 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000725
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000726 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000727 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000728
729 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000730 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000731 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000732 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000733 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000734 except presubmit_support.PresubmitFailure, e:
735 DieWithError(
736 ('%s\nMaybe your depot_tools is out of date?\n'
737 'If all fails, contact maruel@') % e)
738
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000739 def UpdateDescription(self, description):
740 self.description = description
741 return self.RpcServer().update_description(
742 self.GetIssue(), self.description)
743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000745 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000746 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000748 def SetFlag(self, flag, value):
749 """Patchset must match."""
750 if not self.GetPatchset():
751 DieWithError('The patchset needs to match. Send another patchset.')
752 try:
753 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000754 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000755 except urllib2.HTTPError, e:
756 if e.code == 404:
757 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
758 if e.code == 403:
759 DieWithError(
760 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
761 'match?') % (self.GetIssue(), self.GetPatchset()))
762 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000764 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 """Returns an upload.RpcServer() to access this review's rietveld instance.
766 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000767 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000768 self._rpc_server = rietveld.CachingRietveld(
769 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000770 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771
772 def _IssueSetting(self):
773 """Return the git setting that stores this change's issue."""
774 return 'branch.%s.rietveldissue' % self.GetBranch()
775
776 def _PatchsetSetting(self):
777 """Return the git setting that stores this change's most recent patchset."""
778 return 'branch.%s.rietveldpatchset' % self.GetBranch()
779
780 def _RietveldServer(self):
781 """Returns the git setting that stores this change's rietveld server."""
782 return 'branch.%s.rietveldserver' % self.GetBranch()
783
784
785def GetCodereviewSettingsInteractively():
786 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000787 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 server = settings.GetDefaultServerUrl(error_ok=True)
789 prompt = 'Rietveld server (host[:port])'
790 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000791 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 if not server and not newserver:
793 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000794 if newserver:
795 newserver = gclient_utils.UpgradeToHttps(newserver)
796 if newserver != server:
797 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000799 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 prompt = caption
801 if initial:
802 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000803 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000804 if new_val == 'x':
805 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000806 elif new_val:
807 if is_url:
808 new_val = gclient_utils.UpgradeToHttps(new_val)
809 if new_val != initial:
810 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000812 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000813 SetProperty(settings.GetDefaultPrivateFlag(),
814 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000816 'tree-status-url', False)
817 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818
819 # TODO: configure a default branch to diff against, rather than this
820 # svn-based hackery.
821
822
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000823class ChangeDescription(object):
824 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000825 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000826
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000827 def __init__(self, description):
828 self._description = (description or '').strip()
829
830 @property
831 def description(self):
832 return self._description
833
834 def update_reviewers(self, reviewers):
835 """Rewrites the R=/TBR= line(s) as a single line."""
836 assert isinstance(reviewers, list), reviewers
837 if not reviewers:
838 return
839 regexp = re.compile(self.R_LINE, re.MULTILINE)
840 matches = list(regexp.finditer(self._description))
841 is_tbr = any(m.group(1) == 'TBR' for m in matches)
842 if len(matches) > 1:
843 # Erase all except the first one.
844 for i in xrange(len(matches) - 1, 0, -1):
845 self._description = (
846 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000847 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000848
849 if is_tbr:
850 new_r_line = 'TBR=' + ', '.join(reviewers)
851 else:
852 new_r_line = 'R=' + ', '.join(reviewers)
853
854 if matches:
855 self._description = (
856 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000857 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000858 else:
859 self.append_footer(new_r_line)
860
861 def prompt(self):
862 """Asks the user to update the description."""
863 self._description = (
864 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000865 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000866 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000867 '#--------------------This line is 72 characters long'
868 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000869 ) + self._description
870
871 if '\nBUG=' not in self._description:
872 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000873 content = gclient_utils.RunEditor(self._description, True,
874 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000875 if not content:
876 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000877
878 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000879 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000881 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000883
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000884 def append_footer(self, line):
885 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
886 if self._description:
887 if '\n' not in self._description:
888 self._description += '\n'
889 else:
890 last_line = self._description.rsplit('\n', 1)[1]
891 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
892 not presubmit_support.Change.TAG_LINE_RE.match(line)):
893 self._description += '\n'
894 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000895
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000896 def get_reviewers(self):
897 """Retrieves the list of reviewers."""
898 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000899 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000900 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000901
902
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000903def get_approving_reviewers(props):
904 """Retrieves the reviewers that approved a CL from the issue properties with
905 messages.
906
907 Note that the list may contain reviewers that are not committer, thus are not
908 considered by the CQ.
909 """
910 return sorted(
911 set(
912 message['sender']
913 for message in props['messages']
914 if message['approval'] and message['sender'] in props['reviewers']
915 )
916 )
917
918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919def FindCodereviewSettingsFile(filename='codereview.settings'):
920 """Finds the given file starting in the cwd and going up.
921
922 Only looks up to the top of the repository unless an
923 'inherit-review-settings-ok' file exists in the root of the repository.
924 """
925 inherit_ok_file = 'inherit-review-settings-ok'
926 cwd = os.getcwd()
927 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
928 if os.path.isfile(os.path.join(root, inherit_ok_file)):
929 root = '/'
930 while True:
931 if filename in os.listdir(cwd):
932 if os.path.isfile(os.path.join(cwd, filename)):
933 return open(os.path.join(cwd, filename))
934 if cwd == root:
935 break
936 cwd = os.path.dirname(cwd)
937
938
939def LoadCodereviewSettingsFromFile(fileobj):
940 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000941 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943 def SetProperty(name, setting, unset_error_ok=False):
944 fullname = 'rietveld.' + name
945 if setting in keyvals:
946 RunGit(['config', fullname, keyvals[setting]])
947 else:
948 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
949
950 SetProperty('server', 'CODE_REVIEW_SERVER')
951 # Only server setting is required. Other settings can be absent.
952 # In that case, we ignore errors raised during option deletion attempt.
953 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000954 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
956 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
957
ukai@chromium.orge8077812012-02-03 03:41:46 +0000958 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
959 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
960 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
963 #should be of the form
964 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
965 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
966 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
967 keyvals['ORIGIN_URL_CONFIG']])
968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000970def urlretrieve(source, destination):
971 """urllib is broken for SSL connections via a proxy therefore we
972 can't use urllib.urlretrieve()."""
973 with open(destination, 'w') as f:
974 f.write(urllib2.urlopen(source).read())
975
976
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000977def DownloadHooks(force):
978 """downloads hooks
979
980 Args:
981 force: True to update hooks. False to install hooks if not present.
982 """
983 if not settings.GetIsGerrit():
984 return
985 server_url = settings.GetDefaultServerUrl()
986 src = '%s/tools/hooks/commit-msg' % server_url
987 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
988 if not os.access(dst, os.X_OK):
989 if os.path.exists(dst):
990 if not force:
991 return
992 os.remove(dst)
993 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000994 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000995 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
996 except Exception:
997 if os.path.exists(dst):
998 os.remove(dst)
999 DieWithError('\nFailed to download hooks from %s' % src)
1000
1001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002@usage('[repo root containing codereview.settings]')
1003def CMDconfig(parser, args):
1004 """edit configuration for this tree"""
1005
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001006 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007 if len(args) == 0:
1008 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001009 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 return 0
1011
1012 url = args[0]
1013 if not url.endswith('codereview.settings'):
1014 url = os.path.join(url, 'codereview.settings')
1015
1016 # Load code review settings and download hooks (if available).
1017 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001018 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019 return 0
1020
1021
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001022def CMDbaseurl(parser, args):
1023 """get or set base-url for this branch"""
1024 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1025 branch = ShortBranchName(branchref)
1026 _, args = parser.parse_args(args)
1027 if not args:
1028 print("Current base-url:")
1029 return RunGit(['config', 'branch.%s.base-url' % branch],
1030 error_ok=False).strip()
1031 else:
1032 print("Setting base-url to %s" % args[0])
1033 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1034 error_ok=False).strip()
1035
1036
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037def CMDstatus(parser, args):
1038 """show status of changelists"""
1039 parser.add_option('--field',
1040 help='print only specific field (desc|id|patch|url)')
1041 (options, args) = parser.parse_args(args)
1042
1043 # TODO: maybe make show_branches a flag if necessary.
1044 show_branches = not options.field
1045
1046 if show_branches:
1047 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1048 if branches:
1049 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001050 changes = (Changelist(branchref=b) for b in branches.splitlines())
1051 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1052 alignment = max(5, max(len(b) for b in branches))
1053 for branch in sorted(branches):
1054 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055
1056 cl = Changelist()
1057 if options.field:
1058 if options.field.startswith('desc'):
1059 print cl.GetDescription()
1060 elif options.field == 'id':
1061 issueid = cl.GetIssue()
1062 if issueid:
1063 print issueid
1064 elif options.field == 'patch':
1065 patchset = cl.GetPatchset()
1066 if patchset:
1067 print patchset
1068 elif options.field == 'url':
1069 url = cl.GetIssueURL()
1070 if url:
1071 print url
1072 else:
1073 print
1074 print 'Current branch:',
1075 if not cl.GetIssue():
1076 print 'no issue assigned.'
1077 return 0
1078 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001079 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 print 'Issue description:'
1081 print cl.GetDescription(pretty=True)
1082 return 0
1083
1084
1085@usage('[issue_number]')
1086def CMDissue(parser, args):
1087 """Set or display the current code review issue number.
1088
1089 Pass issue number 0 to clear the current issue.
1090"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001091 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092
1093 cl = Changelist()
1094 if len(args) > 0:
1095 try:
1096 issue = int(args[0])
1097 except ValueError:
1098 DieWithError('Pass a number to set the issue or none to list it.\n'
1099 'Maybe you want to run git cl status?')
1100 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001101 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102 return 0
1103
1104
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001105def CMDcomments(parser, args):
1106 """show review comments of the current changelist"""
1107 (_, args) = parser.parse_args(args)
1108 if args:
1109 parser.error('Unsupported argument: %s' % args)
1110
1111 cl = Changelist()
1112 if cl.GetIssue():
1113 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1114 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001115 if message['disapproval']:
1116 color = Fore.RED
1117 elif message['approval']:
1118 color = Fore.GREEN
1119 elif message['sender'] == data['owner_email']:
1120 color = Fore.MAGENTA
1121 else:
1122 color = Fore.BLUE
1123 print '\n%s%s %s%s' % (
1124 color, message['date'].split('.', 1)[0], message['sender'],
1125 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001126 if message['text'].strip():
1127 print '\n'.join(' ' + l for l in message['text'].splitlines())
1128 return 0
1129
1130
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001131def CMDdescription(parser, args):
1132 """brings up the editor for the current CL's description."""
1133 cl = Changelist()
1134 if not cl.GetIssue():
1135 DieWithError('This branch has no associated changelist.')
1136 description = ChangeDescription(cl.GetDescription())
1137 description.prompt()
1138 cl.UpdateDescription(description.description)
1139 return 0
1140
1141
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142def CreateDescriptionFromLog(args):
1143 """Pulls out the commit log to use as a base for the CL description."""
1144 log_args = []
1145 if len(args) == 1 and not args[0].endswith('.'):
1146 log_args = [args[0] + '..']
1147 elif len(args) == 1 and args[0].endswith('...'):
1148 log_args = [args[0][:-1]]
1149 elif len(args) == 2:
1150 log_args = [args[0] + '..' + args[1]]
1151 else:
1152 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001153 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154
1155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156def CMDpresubmit(parser, args):
1157 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001158 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001160 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001161 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 (options, args) = parser.parse_args(args)
1163
ukai@chromium.org259e4682012-10-25 07:36:33 +00001164 if not options.force and is_dirty_git_tree('presubmit'):
1165 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 return 1
1167
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001168 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 if args:
1170 base_branch = args[0]
1171 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001172 # Default to diffing against the common ancestor of the upstream branch.
1173 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001175 cl.RunHook(
1176 committing=not options.upload,
1177 may_prompt=False,
1178 verbose=options.verbose,
1179 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001180 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181
1182
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001183def AddChangeIdToCommitMessage(options, args):
1184 """Re-commits using the current message, assumes the commit hook is in
1185 place.
1186 """
1187 log_desc = options.message or CreateDescriptionFromLog(args)
1188 git_command = ['commit', '--amend', '-m', log_desc]
1189 RunGit(git_command)
1190 new_log_desc = CreateDescriptionFromLog(args)
1191 if CHANGE_ID in new_log_desc:
1192 print 'git-cl: Added Change-Id to commit message.'
1193 else:
1194 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1195
1196
ukai@chromium.orge8077812012-02-03 03:41:46 +00001197def GerritUpload(options, args, cl):
1198 """upload the current branch to gerrit."""
1199 # We assume the remote called "origin" is the one we want.
1200 # It is probably not worthwhile to support different workflows.
1201 remote = 'origin'
1202 branch = 'master'
1203 if options.target_branch:
1204 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001206 change_desc = ChangeDescription(
1207 options.message or CreateDescriptionFromLog(args))
1208 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001209 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001211 if CHANGE_ID not in change_desc.description:
1212 AddChangeIdToCommitMessage(options, args)
1213 if options.reviewers:
1214 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
ukai@chromium.orge8077812012-02-03 03:41:46 +00001216 receive_options = []
1217 cc = cl.GetCCList().split(',')
1218 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001219 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001220 cc = filter(None, cc)
1221 if cc:
1222 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001223 if change_desc.get_reviewers():
1224 receive_options.extend(
1225 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226
ukai@chromium.orge8077812012-02-03 03:41:46 +00001227 git_command = ['push']
1228 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001229 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001230 ' '.join(receive_options))
1231 git_command += [remote, 'HEAD:refs/for/' + branch]
1232 RunGit(git_command)
1233 # TODO(ukai): parse Change-Id: and set issue number?
1234 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001235
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236
ukai@chromium.orge8077812012-02-03 03:41:46 +00001237def RietveldUpload(options, args, cl):
1238 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1240 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 if options.emulate_svn_auto_props:
1242 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243
1244 change_desc = None
1245
1246 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001247 if options.title:
1248 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001249 if options.message:
1250 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001251 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 print ("This branch is associated with issue %s. "
1253 "Adding patch to that issue." % cl.GetIssue())
1254 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001255 if options.title:
1256 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001257 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001258 change_desc = ChangeDescription(message)
1259 if options.reviewers:
1260 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001261 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001262 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001263
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001264 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 print "Description is empty; aborting."
1266 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001267
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001268 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001269 if change_desc.get_reviewers():
1270 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001271 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001272 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001273 DieWithError("Must specify reviewers to send email.")
1274 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001275 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001276 if cc:
1277 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001279 if options.private or settings.GetDefaultPrivateFlag() == "True":
1280 upload_args.append('--private')
1281
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001282 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001283 if not options.find_copies:
1284 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 # Include the upstream repo's URL in the change -- this is useful for
1287 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001288 remote_url = cl.GetGitBaseUrlFromConfig()
1289 if not remote_url:
1290 if settings.GetIsGitSvn():
1291 # URL is dependent on the current directory.
1292 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1293 if data:
1294 keys = dict(line.split(': ', 1) for line in data.splitlines()
1295 if ': ' in line)
1296 remote_url = keys.get('URL', None)
1297 else:
1298 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1299 remote_url = (cl.GetRemoteUrl() + '@'
1300 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 if remote_url:
1302 upload_args.extend(['--base_url', remote_url])
1303
1304 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001305 upload_args = ['upload'] + upload_args + args
1306 logging.info('upload.RealMain(%s)', upload_args)
1307 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001308 except KeyboardInterrupt:
1309 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 except:
1311 # If we got an exception after the user typed a description for their
1312 # change, back up the description before re-raising.
1313 if change_desc:
1314 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1315 print '\nGot exception while uploading -- saving description to %s\n' \
1316 % backup_path
1317 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001318 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319 backup_file.close()
1320 raise
1321
1322 if not cl.GetIssue():
1323 cl.SetIssue(issue)
1324 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001325
1326 if options.use_commit_queue:
1327 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 return 0
1329
1330
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001331def cleanup_list(l):
1332 """Fixes a list so that comma separated items are put as individual items.
1333
1334 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1335 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1336 """
1337 items = sum((i.split(',') for i in l), [])
1338 stripped_items = (i.strip() for i in items)
1339 return sorted(filter(None, stripped_items))
1340
1341
ukai@chromium.orge8077812012-02-03 03:41:46 +00001342@usage('[args to "git diff"]')
1343def CMDupload(parser, args):
1344 """upload the current changelist to codereview"""
1345 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1346 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001347 parser.add_option('--bypass-watchlists', action='store_true',
1348 dest='bypass_watchlists',
1349 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001350 parser.add_option('-f', action='store_true', dest='force',
1351 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001352 parser.add_option('-m', dest='message', help='message for patchset')
1353 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001354 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001355 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001356 help='reviewer email addresses')
1357 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001358 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001359 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001360 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001361 help='send email to reviewer immediately')
1362 parser.add_option("--emulate_svn_auto_props", action="store_true",
1363 dest="emulate_svn_auto_props",
1364 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001365 parser.add_option('-c', '--use-commit-queue', action='store_true',
1366 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001367 parser.add_option('--private', action='store_true',
1368 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001369 parser.add_option('--target_branch',
1370 help='When uploading to gerrit, remote branch to '
1371 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001372 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001373 (options, args) = parser.parse_args(args)
1374
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001375 if options.target_branch and not settings.GetIsGerrit():
1376 parser.error('Use --target_branch for non gerrit repository.')
1377
ukai@chromium.org259e4682012-10-25 07:36:33 +00001378 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001379 return 1
1380
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001381 options.reviewers = cleanup_list(options.reviewers)
1382 options.cc = cleanup_list(options.cc)
1383
ukai@chromium.orge8077812012-02-03 03:41:46 +00001384 cl = Changelist()
1385 if args:
1386 # TODO(ukai): is it ok for gerrit case?
1387 base_branch = args[0]
1388 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001389 # Default to diffing against common ancestor of upstream branch
1390 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001391 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001392
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001393 # Apply watchlists on upload.
1394 change = cl.GetChange(base_branch, None)
1395 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1396 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001397 if not options.bypass_watchlists:
1398 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001399
ukai@chromium.orge8077812012-02-03 03:41:46 +00001400 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001401 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001402 may_prompt=not options.force,
1403 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001404 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001405 if not hook_results.should_continue():
1406 return 1
1407 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001408 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001409
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001410 if cl.GetIssue():
1411 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1412 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001413 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001414 print ('The last upload made from this repository was patchset #%d but '
1415 'the most recent patchset on the server is #%d.'
1416 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001417 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1418 'from another machine or branch the patch you\'re uploading now '
1419 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001420 ask_for_data('About to upload; enter to confirm.')
1421
iannucci@chromium.org79540052012-10-19 23:15:26 +00001422 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001423 if settings.GetIsGerrit():
1424 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001425 ret = RietveldUpload(options, args, cl)
1426 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001427 git_set_branch_value('last-upload-hash',
1428 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001429
1430 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001431
1432
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001433def IsSubmoduleMergeCommit(ref):
1434 # When submodules are added to the repo, we expect there to be a single
1435 # non-git-svn merge commit at remote HEAD with a signature comment.
1436 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001437 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001438 return RunGit(cmd) != ''
1439
1440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441def SendUpstream(parser, args, cmd):
1442 """Common code for CmdPush and CmdDCommit
1443
1444 Squashed commit into a single.
1445 Updates changelog with metadata (e.g. pointer to review).
1446 Pushes/dcommits the code upstream.
1447 Updates review and closes.
1448 """
1449 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1450 help='bypass upload presubmit hook')
1451 parser.add_option('-m', dest='message',
1452 help="override review description")
1453 parser.add_option('-f', action='store_true', dest='force',
1454 help="force yes to questions (don't prompt)")
1455 parser.add_option('-c', dest='contributor',
1456 help="external contributor for patch (appended to " +
1457 "description and used as author for git). Should be " +
1458 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001459 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 (options, args) = parser.parse_args(args)
1461 cl = Changelist()
1462
1463 if not args or cmd == 'push':
1464 # Default to merging against our best guess of the upstream branch.
1465 args = [cl.GetUpstreamBranch()]
1466
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001467 if options.contributor:
1468 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1469 print "Please provide contibutor as 'First Last <email@example.com>'"
1470 return 1
1471
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001473 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474
ukai@chromium.org259e4682012-10-25 07:36:33 +00001475 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476 return 1
1477
1478 # This rev-list syntax means "show all commits not in my branch that
1479 # are in base_branch".
1480 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1481 base_branch]).splitlines()
1482 if upstream_commits:
1483 print ('Base branch "%s" has %d commits '
1484 'not in this branch.' % (base_branch, len(upstream_commits)))
1485 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1486 return 1
1487
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001488 # This is the revision `svn dcommit` will commit on top of.
1489 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1490 '--pretty=format:%H'])
1491
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001493 # If the base_head is a submodule merge commit, the first parent of the
1494 # base_head should be a git-svn commit, which is what we're interested in.
1495 base_svn_head = base_branch
1496 if base_has_submodules:
1497 base_svn_head += '^1'
1498
1499 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500 if extra_commits:
1501 print ('This branch has %d additional commits not upstreamed yet.'
1502 % len(extra_commits.splitlines()))
1503 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1504 'before attempting to %s.' % (base_branch, cmd))
1505 return 1
1506
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001507 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001508 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001509 author = None
1510 if options.contributor:
1511 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001512 hook_results = cl.RunHook(
1513 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001514 may_prompt=not options.force,
1515 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001516 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001517 if not hook_results.should_continue():
1518 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519
1520 if cmd == 'dcommit':
1521 # Check the tree status if the tree status URL is set.
1522 status = GetTreeStatus()
1523 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001524 print('The tree is closed. Please wait for it to reopen. Use '
1525 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526 return 1
1527 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001528 print('Unable to determine tree status. Please verify manually and '
1529 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001530 else:
1531 breakpad.SendStack(
1532 'GitClHooksBypassedCommit',
1533 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001534 (cl.GetRietveldServer(), cl.GetIssue()),
1535 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 change_desc = ChangeDescription(options.message)
1538 if not change_desc.description and cl.GetIssue():
1539 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001541 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001542 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001543 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001544 else:
1545 print 'No description set.'
1546 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1547 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001549 # Keep a separate copy for the commit message, because the commit message
1550 # contains the link to the Rietveld issue, while the Rietveld message contains
1551 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001552 # Keep a separate copy for the commit message.
1553 if cl.GetIssue():
1554 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1555
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001556 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001557 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001558 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001560 commit_desc.append_footer('Patch from %s.' % options.contributor)
1561
1562 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563
1564 branches = [base_branch, cl.GetBranchRef()]
1565 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001566 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001567 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001568
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001569 # We want to squash all this branch's commits into one commit with the proper
1570 # description. We do this by doing a "reset --soft" to the base branch (which
1571 # keeps the working copy the same), then dcommitting that. If origin/master
1572 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1573 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001574 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001575 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1576 # Delete the branches if they exist.
1577 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1578 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1579 result = RunGitWithCode(showref_cmd)
1580 if result[0] == 0:
1581 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582
1583 # We might be in a directory that's present in this branch but not in the
1584 # trunk. Move up to the top of the tree so that git commands that expect a
1585 # valid CWD won't fail after we check out the merge branch.
1586 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1587 if rel_base_path:
1588 os.chdir(rel_base_path)
1589
1590 # Stuff our change into the merge branch.
1591 # We wrap in a try...finally block so if anything goes wrong,
1592 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001593 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001594 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001595 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1596 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001598 RunGit(
1599 [
1600 'commit', '--author', options.contributor,
1601 '-m', commit_desc.description,
1602 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001603 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001604 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001605 if base_has_submodules:
1606 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1607 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1608 RunGit(['checkout', CHERRY_PICK_BRANCH])
1609 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610 if cmd == 'push':
1611 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001612 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613 retcode, output = RunGitWithCode(
1614 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1615 logging.debug(output)
1616 else:
1617 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001618 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001619 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001620 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621 finally:
1622 # And then swap back to the original branch and clean up.
1623 RunGit(['checkout', '-q', cl.GetBranch()])
1624 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001625 if base_has_submodules:
1626 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001627
1628 if cl.GetIssue():
1629 if cmd == 'dcommit' and 'Committed r' in output:
1630 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1631 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001632 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1633 for l in output.splitlines(False))
1634 match = filter(None, match)
1635 if len(match) != 1:
1636 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1637 output)
1638 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 else:
1640 return 1
1641 viewvc_url = settings.GetViewVCUrl()
1642 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001643 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001644 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001645 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646 print ('Closing issue '
1647 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001648 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001650 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001651 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001652 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001653 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1654 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001656
1657 if retcode == 0:
1658 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1659 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001660 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001661
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662 return 0
1663
1664
1665@usage('[upstream branch to apply against]')
1666def CMDdcommit(parser, args):
1667 """commit the current changelist via git-svn"""
1668 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001669 message = """This doesn't appear to be an SVN repository.
1670If your project has a git mirror with an upstream SVN master, you probably need
1671to run 'git svn init', see your project's git mirror documentation.
1672If your project has a true writeable upstream repository, you probably want
1673to run 'git cl push' instead.
1674Choose wisely, if you get this wrong, your commit might appear to succeed but
1675will instead be silently ignored."""
1676 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001677 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001678 return SendUpstream(parser, args, 'dcommit')
1679
1680
1681@usage('[upstream branch to apply against]')
1682def CMDpush(parser, args):
1683 """commit the current changelist via git"""
1684 if settings.GetIsGitSvn():
1685 print('This appears to be an SVN repository.')
1686 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001687 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001688 return SendUpstream(parser, args, 'push')
1689
1690
1691@usage('<patch url or issue id>')
1692def CMDpatch(parser, args):
1693 """patch in a code review"""
1694 parser.add_option('-b', dest='newbranch',
1695 help='create a new branch off trunk for the patch')
1696 parser.add_option('-f', action='store_true', dest='force',
1697 help='with -b, clobber any existing branch')
1698 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org0bdc2652013-06-07 23:47:05 +00001699 help='allow failed patches and spew .rej files')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1701 help="don't commit after patch applies")
1702 (options, args) = parser.parse_args(args)
1703 if len(args) != 1:
1704 parser.print_help()
1705 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001706 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001708 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001709 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001710
maruel@chromium.org52424302012-08-29 15:14:30 +00001711 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001712 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001713 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001714 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001715 patchset = cl.GetMostRecentPatchset(issue)
1716 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001718 # Assume it's a URL to the patch. Default to https.
1719 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001720 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001721 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 DieWithError('Must pass an issue ID or full URL for '
1723 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001724 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001725 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001726 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001727
1728 if options.newbranch:
1729 if options.force:
1730 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001731 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732 RunGit(['checkout', '-b', options.newbranch,
1733 Changelist().GetUpstreamBranch()])
1734
1735 # Switch up to the top-level directory, if necessary, in preparation for
1736 # applying the patch.
1737 top = RunGit(['rev-parse', '--show-cdup']).strip()
1738 if top:
1739 os.chdir(top)
1740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001741 # Git patches have a/ at the beginning of source paths. We strip that out
1742 # with a sed script rather than the -p flag to patch so we can feed either
1743 # Git or svn-style patches into the same apply command.
1744 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001745 try:
1746 patch_data = subprocess2.check_output(
1747 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1748 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749 DieWithError('Git patch mungling failed.')
1750 logging.info(patch_data)
1751 # We use "git apply" to apply the patch instead of "patch" so that we can
1752 # pick up file adds.
1753 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001754 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 if options.reject:
1756 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001757 try:
1758 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1759 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760 DieWithError('Failed to apply the patch')
1761
1762 # If we had an issue, commit the current state and register the issue.
1763 if not options.nocommit:
1764 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1765 cl = Changelist()
1766 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001767 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001768 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769 else:
1770 print "Patch applied to index."
1771 return 0
1772
1773
1774def CMDrebase(parser, args):
1775 """rebase current branch on top of svn repo"""
1776 # Provide a wrapper for git svn rebase to help avoid accidental
1777 # git svn dcommit.
1778 # It's the only command that doesn't use parser at all since we just defer
1779 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001780 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781
1782
1783def GetTreeStatus():
1784 """Fetches the tree status and returns either 'open', 'closed',
1785 'unknown' or 'unset'."""
1786 url = settings.GetTreeStatusUrl(error_ok=True)
1787 if url:
1788 status = urllib2.urlopen(url).read().lower()
1789 if status.find('closed') != -1 or status == '0':
1790 return 'closed'
1791 elif status.find('open') != -1 or status == '1':
1792 return 'open'
1793 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794 return 'unset'
1795
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797def GetTreeStatusReason():
1798 """Fetches the tree status from a json url and returns the message
1799 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001800 url = settings.GetTreeStatusUrl()
1801 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001802 connection = urllib2.urlopen(json_url)
1803 status = json.loads(connection.read())
1804 connection.close()
1805 return status['message']
1806
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808def CMDtree(parser, args):
1809 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001810 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811 status = GetTreeStatus()
1812 if 'unset' == status:
1813 print 'You must configure your tree status URL by running "git cl config".'
1814 return 2
1815
1816 print "The tree is %s" % status
1817 print
1818 print GetTreeStatusReason()
1819 if status != 'open':
1820 return 1
1821 return 0
1822
1823
maruel@chromium.org15192402012-09-06 12:38:29 +00001824def CMDtry(parser, args):
1825 """Triggers a try job through Rietveld."""
1826 group = optparse.OptionGroup(parser, "Try job options")
1827 group.add_option(
1828 "-b", "--bot", action="append",
1829 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1830 "times to specify multiple builders. ex: "
1831 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1832 "the try server waterfall for the builders name and the tests "
1833 "available. Can also be used to specify gtest_filter, e.g. "
1834 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1835 group.add_option(
1836 "-r", "--revision",
1837 help="Revision to use for the try job; default: the "
1838 "revision will be determined by the try server; see "
1839 "its waterfall for more info")
1840 group.add_option(
1841 "-c", "--clobber", action="store_true", default=False,
1842 help="Force a clobber before building; e.g. don't do an "
1843 "incremental build")
1844 group.add_option(
1845 "--project",
1846 help="Override which project to use. Projects are defined "
1847 "server-side to define what default bot set to use")
1848 group.add_option(
1849 "-t", "--testfilter", action="append", default=[],
1850 help=("Apply a testfilter to all the selected builders. Unless the "
1851 "builders configurations are similar, use multiple "
1852 "--bot <builder>:<test> arguments."))
1853 group.add_option(
1854 "-n", "--name", help="Try job name; default to current branch name")
1855 parser.add_option_group(group)
1856 options, args = parser.parse_args(args)
1857
1858 if args:
1859 parser.error('Unknown arguments: %s' % args)
1860
1861 cl = Changelist()
1862 if not cl.GetIssue():
1863 parser.error('Need to upload first')
1864
1865 if not options.name:
1866 options.name = cl.GetBranch()
1867
1868 # Process --bot and --testfilter.
1869 if not options.bot:
1870 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001871 change = cl.GetChange(
1872 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1873 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001874 options.bot = presubmit_support.DoGetTrySlaves(
1875 change,
1876 change.LocalPaths(),
1877 settings.GetRoot(),
1878 None,
1879 None,
1880 options.verbose,
1881 sys.stdout)
1882 if not options.bot:
1883 parser.error('No default try builder to try, use --bot')
1884
1885 builders_and_tests = {}
1886 for bot in options.bot:
1887 if ':' in bot:
1888 builder, tests = bot.split(':', 1)
1889 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1890 elif ',' in bot:
1891 parser.error('Specify one bot per --bot flag')
1892 else:
1893 builders_and_tests.setdefault(bot, []).append('defaulttests')
1894
1895 if options.testfilter:
1896 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1897 builders_and_tests = dict(
1898 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1899 if t != ['compile'])
1900
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001901 if any('triggered' in b for b in builders_and_tests):
1902 print >> sys.stderr, (
1903 'ERROR You are trying to send a job to a triggered bot. This type of'
1904 ' bot requires an\ninitial job from a parent (usually a builder). '
1905 'Instead send your job to the parent.\n'
1906 'Bot list: %s' % builders_and_tests)
1907 return 1
1908
maruel@chromium.org15192402012-09-06 12:38:29 +00001909 patchset = cl.GetPatchset()
1910 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001911 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001912
1913 cl.RpcServer().trigger_try_jobs(
1914 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1915 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001916 print('Tried jobs on:')
1917 length = max(len(builder) for builder in builders_and_tests)
1918 for builder in sorted(builders_and_tests):
1919 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001920 return 0
1921
1922
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001923@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001925 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001926 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001927 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001928 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001929 return 0
1930
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001932 if args:
1933 # One arg means set upstream branch.
1934 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1935 cl = Changelist()
1936 print "Upstream branch set to " + cl.GetUpstreamBranch()
1937 else:
1938 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001939 return 0
1940
1941
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001942def CMDset_commit(parser, args):
1943 """set the commit bit"""
1944 _, args = parser.parse_args(args)
1945 if args:
1946 parser.error('Unrecognized args: %s' % ' '.join(args))
1947 cl = Changelist()
1948 cl.SetFlag('commit', '1')
1949 return 0
1950
1951
groby@chromium.org411034a2013-02-26 15:12:01 +00001952def CMDset_close(parser, args):
1953 """close the issue"""
1954 _, args = parser.parse_args(args)
1955 if args:
1956 parser.error('Unrecognized args: %s' % ' '.join(args))
1957 cl = Changelist()
1958 # Ensure there actually is an issue to close.
1959 cl.GetDescription()
1960 cl.CloseIssue()
1961 return 0
1962
1963
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001964def CMDformat(parser, args):
1965 """run clang-format on the diff"""
1966 CLANG_EXTS = ['.cc', '.cpp', '.h']
1967 parser.add_option('--full', action='store_true', default=False)
1968 opts, args = parser.parse_args(args)
1969 if args:
1970 parser.error('Unrecognized args: %s' % ' '.join(args))
1971
digit@chromium.org29e47272013-05-17 17:01:46 +00001972 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00001973 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001974 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00001975 # Only list the names of modified files.
1976 diff_cmd.append('--name-only')
1977 else:
1978 # Only generate context-less patches.
1979 diff_cmd.append('-U0')
1980
1981 # Grab the merge-base commit, i.e. the upstream commit of the current
1982 # branch when it was created or the last time it was rebased. This is
1983 # to cover the case where the user may have called "git fetch origin",
1984 # moving the origin branch to a newer commit, but hasn't rebased yet.
1985 upstream_commit = None
1986 cl = Changelist()
1987 upstream_branch = cl.GetUpstreamBranch()
1988 if upstream_branch:
1989 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
1990 upstream_commit = upstream_commit.strip()
1991
1992 if not upstream_commit:
1993 DieWithError('Could not find base commit for this branch. '
1994 'Are you in detached state?')
1995
1996 diff_cmd.append(upstream_commit)
1997
1998 # Handle source file filtering.
1999 diff_cmd.append('--')
2000 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2001 diff_output = RunGit(diff_cmd)
2002
2003 if opts.full:
2004 # diff_output is a list of files to send to clang-format.
2005 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002006 if not files:
2007 print "Nothing to format."
2008 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002009 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002010 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002011 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002012 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2013 'clang-format-diff.py')
2014 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002015 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2016 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2017 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002018
2019 return 0
2020
2021
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022def Command(name):
2023 return getattr(sys.modules[__name__], 'CMD' + name, None)
2024
2025
2026def CMDhelp(parser, args):
2027 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002028 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029 if len(args) == 1:
2030 return main(args + ['--help'])
2031 parser.print_help()
2032 return 0
2033
2034
2035def GenUsage(parser, command):
2036 """Modify an OptParse object with the function's documentation."""
2037 obj = Command(command)
2038 more = getattr(obj, 'usage_more', '')
2039 if command == 'help':
2040 command = '<command>'
2041 else:
2042 # OptParser.description prefer nicely non-formatted strings.
2043 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2044 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2045
2046
2047def main(argv):
2048 """Doesn't parse the arguments here, just find the right subcommand to
2049 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002050 if sys.hexversion < 0x02060000:
2051 print >> sys.stderr, (
2052 '\nYour python version %s is unsupported, please upgrade.\n' %
2053 sys.version.split(' ', 1)[0])
2054 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002055
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002056 # Reload settings.
2057 global settings
2058 settings = Settings()
2059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060 # Do it late so all commands are listed.
2061 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
2062 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
2063 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
2064
2065 # Create the option parse and add --verbose support.
2066 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002067 parser.add_option(
2068 '-v', '--verbose', action='count', default=0,
2069 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070 old_parser_args = parser.parse_args
2071 def Parse(args):
2072 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002073 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002074 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002075 elif options.verbose:
2076 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077 else:
2078 logging.basicConfig(level=logging.WARNING)
2079 return options, args
2080 parser.parse_args = Parse
2081
2082 if argv:
2083 command = Command(argv[0])
2084 if command:
2085 # "fix" the usage and the description now that we know the subcommand.
2086 GenUsage(parser, argv[0])
2087 try:
2088 return command(parser, argv[1:])
2089 except urllib2.HTTPError, e:
2090 if e.code != 500:
2091 raise
2092 DieWithError(
2093 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2094 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2095
2096 # Not a known command. Default to help.
2097 GenUsage(parser, 'help')
2098 return CMDhelp(parser, argv)
2099
2100
2101if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002102 # These affect sys.stdout so do it outside of main() to simplify mocks in
2103 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002104 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002105 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 sys.exit(main(sys.argv[1:]))