blob: 198eb36a5013c10a8003f846091888441999f912 [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'
867 ) + self._description
868
869 if '\nBUG=' not in self._description:
870 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000871 content = gclient_utils.RunEditor(self._description, True,
872 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000873 if not content:
874 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000875
876 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000877 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000878 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000879 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000881
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882 def append_footer(self, line):
883 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
884 if self._description:
885 if '\n' not in self._description:
886 self._description += '\n'
887 else:
888 last_line = self._description.rsplit('\n', 1)[1]
889 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
890 not presubmit_support.Change.TAG_LINE_RE.match(line)):
891 self._description += '\n'
892 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000893
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000894 def get_reviewers(self):
895 """Retrieves the list of reviewers."""
896 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000897 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000898 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000899
900
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000901def get_approving_reviewers(props):
902 """Retrieves the reviewers that approved a CL from the issue properties with
903 messages.
904
905 Note that the list may contain reviewers that are not committer, thus are not
906 considered by the CQ.
907 """
908 return sorted(
909 set(
910 message['sender']
911 for message in props['messages']
912 if message['approval'] and message['sender'] in props['reviewers']
913 )
914 )
915
916
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917def FindCodereviewSettingsFile(filename='codereview.settings'):
918 """Finds the given file starting in the cwd and going up.
919
920 Only looks up to the top of the repository unless an
921 'inherit-review-settings-ok' file exists in the root of the repository.
922 """
923 inherit_ok_file = 'inherit-review-settings-ok'
924 cwd = os.getcwd()
925 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
926 if os.path.isfile(os.path.join(root, inherit_ok_file)):
927 root = '/'
928 while True:
929 if filename in os.listdir(cwd):
930 if os.path.isfile(os.path.join(cwd, filename)):
931 return open(os.path.join(cwd, filename))
932 if cwd == root:
933 break
934 cwd = os.path.dirname(cwd)
935
936
937def LoadCodereviewSettingsFromFile(fileobj):
938 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000939 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941 def SetProperty(name, setting, unset_error_ok=False):
942 fullname = 'rietveld.' + name
943 if setting in keyvals:
944 RunGit(['config', fullname, keyvals[setting]])
945 else:
946 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
947
948 SetProperty('server', 'CODE_REVIEW_SERVER')
949 # Only server setting is required. Other settings can be absent.
950 # In that case, we ignore errors raised during option deletion attempt.
951 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000952 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
954 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
955
ukai@chromium.orge8077812012-02-03 03:41:46 +0000956 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
957 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
958 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000959
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
961 #should be of the form
962 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
963 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
964 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
965 keyvals['ORIGIN_URL_CONFIG']])
966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000968def urlretrieve(source, destination):
969 """urllib is broken for SSL connections via a proxy therefore we
970 can't use urllib.urlretrieve()."""
971 with open(destination, 'w') as f:
972 f.write(urllib2.urlopen(source).read())
973
974
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000975def DownloadHooks(force):
976 """downloads hooks
977
978 Args:
979 force: True to update hooks. False to install hooks if not present.
980 """
981 if not settings.GetIsGerrit():
982 return
983 server_url = settings.GetDefaultServerUrl()
984 src = '%s/tools/hooks/commit-msg' % server_url
985 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
986 if not os.access(dst, os.X_OK):
987 if os.path.exists(dst):
988 if not force:
989 return
990 os.remove(dst)
991 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000992 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000993 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
994 except Exception:
995 if os.path.exists(dst):
996 os.remove(dst)
997 DieWithError('\nFailed to download hooks from %s' % src)
998
999
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000@usage('[repo root containing codereview.settings]')
1001def CMDconfig(parser, args):
1002 """edit configuration for this tree"""
1003
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001004 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 if len(args) == 0:
1006 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001007 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 return 0
1009
1010 url = args[0]
1011 if not url.endswith('codereview.settings'):
1012 url = os.path.join(url, 'codereview.settings')
1013
1014 # Load code review settings and download hooks (if available).
1015 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001016 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 return 0
1018
1019
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001020def CMDbaseurl(parser, args):
1021 """get or set base-url for this branch"""
1022 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1023 branch = ShortBranchName(branchref)
1024 _, args = parser.parse_args(args)
1025 if not args:
1026 print("Current base-url:")
1027 return RunGit(['config', 'branch.%s.base-url' % branch],
1028 error_ok=False).strip()
1029 else:
1030 print("Setting base-url to %s" % args[0])
1031 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1032 error_ok=False).strip()
1033
1034
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001035def CMDstatus(parser, args):
1036 """show status of changelists"""
1037 parser.add_option('--field',
1038 help='print only specific field (desc|id|patch|url)')
1039 (options, args) = parser.parse_args(args)
1040
1041 # TODO: maybe make show_branches a flag if necessary.
1042 show_branches = not options.field
1043
1044 if show_branches:
1045 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1046 if branches:
1047 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001048 changes = (Changelist(branchref=b) for b in branches.splitlines())
1049 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1050 alignment = max(5, max(len(b) for b in branches))
1051 for branch in sorted(branches):
1052 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053
1054 cl = Changelist()
1055 if options.field:
1056 if options.field.startswith('desc'):
1057 print cl.GetDescription()
1058 elif options.field == 'id':
1059 issueid = cl.GetIssue()
1060 if issueid:
1061 print issueid
1062 elif options.field == 'patch':
1063 patchset = cl.GetPatchset()
1064 if patchset:
1065 print patchset
1066 elif options.field == 'url':
1067 url = cl.GetIssueURL()
1068 if url:
1069 print url
1070 else:
1071 print
1072 print 'Current branch:',
1073 if not cl.GetIssue():
1074 print 'no issue assigned.'
1075 return 0
1076 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001077 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 print 'Issue description:'
1079 print cl.GetDescription(pretty=True)
1080 return 0
1081
1082
1083@usage('[issue_number]')
1084def CMDissue(parser, args):
1085 """Set or display the current code review issue number.
1086
1087 Pass issue number 0 to clear the current issue.
1088"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001089 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090
1091 cl = Changelist()
1092 if len(args) > 0:
1093 try:
1094 issue = int(args[0])
1095 except ValueError:
1096 DieWithError('Pass a number to set the issue or none to list it.\n'
1097 'Maybe you want to run git cl status?')
1098 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001099 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 return 0
1101
1102
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001103def CMDcomments(parser, args):
1104 """show review comments of the current changelist"""
1105 (_, args) = parser.parse_args(args)
1106 if args:
1107 parser.error('Unsupported argument: %s' % args)
1108
1109 cl = Changelist()
1110 if cl.GetIssue():
1111 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1112 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001113 if message['disapproval']:
1114 color = Fore.RED
1115 elif message['approval']:
1116 color = Fore.GREEN
1117 elif message['sender'] == data['owner_email']:
1118 color = Fore.MAGENTA
1119 else:
1120 color = Fore.BLUE
1121 print '\n%s%s %s%s' % (
1122 color, message['date'].split('.', 1)[0], message['sender'],
1123 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001124 if message['text'].strip():
1125 print '\n'.join(' ' + l for l in message['text'].splitlines())
1126 return 0
1127
1128
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001129def CMDdescription(parser, args):
1130 """brings up the editor for the current CL's description."""
1131 cl = Changelist()
1132 if not cl.GetIssue():
1133 DieWithError('This branch has no associated changelist.')
1134 description = ChangeDescription(cl.GetDescription())
1135 description.prompt()
1136 cl.UpdateDescription(description.description)
1137 return 0
1138
1139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140def CreateDescriptionFromLog(args):
1141 """Pulls out the commit log to use as a base for the CL description."""
1142 log_args = []
1143 if len(args) == 1 and not args[0].endswith('.'):
1144 log_args = [args[0] + '..']
1145 elif len(args) == 1 and args[0].endswith('...'):
1146 log_args = [args[0][:-1]]
1147 elif len(args) == 2:
1148 log_args = [args[0] + '..' + args[1]]
1149 else:
1150 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001151 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152
1153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154def CMDpresubmit(parser, args):
1155 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001156 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001158 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001159 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 (options, args) = parser.parse_args(args)
1161
ukai@chromium.org259e4682012-10-25 07:36:33 +00001162 if not options.force and is_dirty_git_tree('presubmit'):
1163 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 return 1
1165
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001166 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167 if args:
1168 base_branch = args[0]
1169 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001170 # Default to diffing against the common ancestor of the upstream branch.
1171 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001173 cl.RunHook(
1174 committing=not options.upload,
1175 may_prompt=False,
1176 verbose=options.verbose,
1177 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001178 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179
1180
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001181def AddChangeIdToCommitMessage(options, args):
1182 """Re-commits using the current message, assumes the commit hook is in
1183 place.
1184 """
1185 log_desc = options.message or CreateDescriptionFromLog(args)
1186 git_command = ['commit', '--amend', '-m', log_desc]
1187 RunGit(git_command)
1188 new_log_desc = CreateDescriptionFromLog(args)
1189 if CHANGE_ID in new_log_desc:
1190 print 'git-cl: Added Change-Id to commit message.'
1191 else:
1192 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1193
1194
ukai@chromium.orge8077812012-02-03 03:41:46 +00001195def GerritUpload(options, args, cl):
1196 """upload the current branch to gerrit."""
1197 # We assume the remote called "origin" is the one we want.
1198 # It is probably not worthwhile to support different workflows.
1199 remote = 'origin'
1200 branch = 'master'
1201 if options.target_branch:
1202 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001204 change_desc = ChangeDescription(
1205 options.message or CreateDescriptionFromLog(args))
1206 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001207 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001209 if CHANGE_ID not in change_desc.description:
1210 AddChangeIdToCommitMessage(options, args)
1211 if options.reviewers:
1212 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213
ukai@chromium.orge8077812012-02-03 03:41:46 +00001214 receive_options = []
1215 cc = cl.GetCCList().split(',')
1216 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001217 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001218 cc = filter(None, cc)
1219 if cc:
1220 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001221 if change_desc.get_reviewers():
1222 receive_options.extend(
1223 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224
ukai@chromium.orge8077812012-02-03 03:41:46 +00001225 git_command = ['push']
1226 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001227 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001228 ' '.join(receive_options))
1229 git_command += [remote, 'HEAD:refs/for/' + branch]
1230 RunGit(git_command)
1231 # TODO(ukai): parse Change-Id: and set issue number?
1232 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
ukai@chromium.orge8077812012-02-03 03:41:46 +00001235def RietveldUpload(options, args, cl):
1236 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1238 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if options.emulate_svn_auto_props:
1240 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241
1242 change_desc = None
1243
1244 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001245 if options.title:
1246 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001247 if options.message:
1248 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001249 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 print ("This branch is associated with issue %s. "
1251 "Adding patch to that issue." % cl.GetIssue())
1252 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001253 if options.title:
1254 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001255 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001256 change_desc = ChangeDescription(message)
1257 if options.reviewers:
1258 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001259 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001260 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001261
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001262 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 print "Description is empty; aborting."
1264 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001265
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001266 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001267 if change_desc.get_reviewers():
1268 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001269 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001270 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001271 DieWithError("Must specify reviewers to send email.")
1272 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001273 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001274 if cc:
1275 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001277 if options.private or settings.GetDefaultPrivateFlag() == "True":
1278 upload_args.append('--private')
1279
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001280 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001281 if not options.find_copies:
1282 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001283
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 # Include the upstream repo's URL in the change -- this is useful for
1285 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001286 remote_url = cl.GetGitBaseUrlFromConfig()
1287 if not remote_url:
1288 if settings.GetIsGitSvn():
1289 # URL is dependent on the current directory.
1290 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1291 if data:
1292 keys = dict(line.split(': ', 1) for line in data.splitlines()
1293 if ': ' in line)
1294 remote_url = keys.get('URL', None)
1295 else:
1296 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1297 remote_url = (cl.GetRemoteUrl() + '@'
1298 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 if remote_url:
1300 upload_args.extend(['--base_url', remote_url])
1301
1302 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001303 upload_args = ['upload'] + upload_args + args
1304 logging.info('upload.RealMain(%s)', upload_args)
1305 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001306 except KeyboardInterrupt:
1307 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 except:
1309 # If we got an exception after the user typed a description for their
1310 # change, back up the description before re-raising.
1311 if change_desc:
1312 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1313 print '\nGot exception while uploading -- saving description to %s\n' \
1314 % backup_path
1315 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001316 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 backup_file.close()
1318 raise
1319
1320 if not cl.GetIssue():
1321 cl.SetIssue(issue)
1322 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001323
1324 if options.use_commit_queue:
1325 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326 return 0
1327
1328
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001329def cleanup_list(l):
1330 """Fixes a list so that comma separated items are put as individual items.
1331
1332 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1333 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1334 """
1335 items = sum((i.split(',') for i in l), [])
1336 stripped_items = (i.strip() for i in items)
1337 return sorted(filter(None, stripped_items))
1338
1339
ukai@chromium.orge8077812012-02-03 03:41:46 +00001340@usage('[args to "git diff"]')
1341def CMDupload(parser, args):
1342 """upload the current changelist to codereview"""
1343 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1344 help='bypass upload presubmit hook')
1345 parser.add_option('-f', action='store_true', dest='force',
1346 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001347 parser.add_option('-m', dest='message', help='message for patchset')
1348 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001349 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001350 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001351 help='reviewer email addresses')
1352 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001353 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001354 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001355 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001356 help='send email to reviewer immediately')
1357 parser.add_option("--emulate_svn_auto_props", action="store_true",
1358 dest="emulate_svn_auto_props",
1359 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001360 parser.add_option('-c', '--use-commit-queue', action='store_true',
1361 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001362 parser.add_option('--private', action='store_true',
1363 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001364 parser.add_option('--target_branch',
1365 help='When uploading to gerrit, remote branch to '
1366 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001367 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001368 (options, args) = parser.parse_args(args)
1369
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001370 if options.target_branch and not settings.GetIsGerrit():
1371 parser.error('Use --target_branch for non gerrit repository.')
1372
ukai@chromium.org259e4682012-10-25 07:36:33 +00001373 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001374 return 1
1375
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001376 options.reviewers = cleanup_list(options.reviewers)
1377 options.cc = cleanup_list(options.cc)
1378
ukai@chromium.orge8077812012-02-03 03:41:46 +00001379 cl = Changelist()
1380 if args:
1381 # TODO(ukai): is it ok for gerrit case?
1382 base_branch = args[0]
1383 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 # Default to diffing against common ancestor of upstream branch
1385 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001386 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001387
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001388 # Apply watchlists on upload.
1389 change = cl.GetChange(base_branch, None)
1390 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1391 files = [f.LocalPath() for f in change.AffectedFiles()]
1392 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1393
ukai@chromium.orge8077812012-02-03 03:41:46 +00001394 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001395 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001396 may_prompt=not options.force,
1397 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001398 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001399 if not hook_results.should_continue():
1400 return 1
1401 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001402 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001403
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001404 if cl.GetIssue():
1405 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1406 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001407 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001408 print ('The last upload made from this repository was patchset #%d but '
1409 'the most recent patchset on the server is #%d.'
1410 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001411 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1412 'from another machine or branch the patch you\'re uploading now '
1413 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001414 ask_for_data('About to upload; enter to confirm.')
1415
iannucci@chromium.org79540052012-10-19 23:15:26 +00001416 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001417 if settings.GetIsGerrit():
1418 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001419 ret = RietveldUpload(options, args, cl)
1420 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001421 git_set_branch_value('last-upload-hash',
1422 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001423
1424 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001425
1426
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001427def IsSubmoduleMergeCommit(ref):
1428 # When submodules are added to the repo, we expect there to be a single
1429 # non-git-svn merge commit at remote HEAD with a signature comment.
1430 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001431 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001432 return RunGit(cmd) != ''
1433
1434
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435def SendUpstream(parser, args, cmd):
1436 """Common code for CmdPush and CmdDCommit
1437
1438 Squashed commit into a single.
1439 Updates changelog with metadata (e.g. pointer to review).
1440 Pushes/dcommits the code upstream.
1441 Updates review and closes.
1442 """
1443 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1444 help='bypass upload presubmit hook')
1445 parser.add_option('-m', dest='message',
1446 help="override review description")
1447 parser.add_option('-f', action='store_true', dest='force',
1448 help="force yes to questions (don't prompt)")
1449 parser.add_option('-c', dest='contributor',
1450 help="external contributor for patch (appended to " +
1451 "description and used as author for git). Should be " +
1452 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001453 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 (options, args) = parser.parse_args(args)
1455 cl = Changelist()
1456
1457 if not args or cmd == 'push':
1458 # Default to merging against our best guess of the upstream branch.
1459 args = [cl.GetUpstreamBranch()]
1460
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001461 if options.contributor:
1462 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1463 print "Please provide contibutor as 'First Last <email@example.com>'"
1464 return 1
1465
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001467 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
ukai@chromium.org259e4682012-10-25 07:36:33 +00001469 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 return 1
1471
1472 # This rev-list syntax means "show all commits not in my branch that
1473 # are in base_branch".
1474 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1475 base_branch]).splitlines()
1476 if upstream_commits:
1477 print ('Base branch "%s" has %d commits '
1478 'not in this branch.' % (base_branch, len(upstream_commits)))
1479 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1480 return 1
1481
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001482 # This is the revision `svn dcommit` will commit on top of.
1483 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1484 '--pretty=format:%H'])
1485
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001487 # If the base_head is a submodule merge commit, the first parent of the
1488 # base_head should be a git-svn commit, which is what we're interested in.
1489 base_svn_head = base_branch
1490 if base_has_submodules:
1491 base_svn_head += '^1'
1492
1493 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001494 if extra_commits:
1495 print ('This branch has %d additional commits not upstreamed yet.'
1496 % len(extra_commits.splitlines()))
1497 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1498 'before attempting to %s.' % (base_branch, cmd))
1499 return 1
1500
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001501 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001502 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001503 author = None
1504 if options.contributor:
1505 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001506 hook_results = cl.RunHook(
1507 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001508 may_prompt=not options.force,
1509 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001510 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001511 if not hook_results.should_continue():
1512 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513
1514 if cmd == 'dcommit':
1515 # Check the tree status if the tree status URL is set.
1516 status = GetTreeStatus()
1517 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001518 print('The tree is closed. Please wait for it to reopen. Use '
1519 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520 return 1
1521 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001522 print('Unable to determine tree status. Please verify manually and '
1523 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001524 else:
1525 breakpad.SendStack(
1526 'GitClHooksBypassedCommit',
1527 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001528 (cl.GetRietveldServer(), cl.GetIssue()),
1529 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001531 change_desc = ChangeDescription(options.message)
1532 if not change_desc.description and cl.GetIssue():
1533 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001535 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001536 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001538 else:
1539 print 'No description set.'
1540 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1541 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001543 # Keep a separate copy for the commit message, because the commit message
1544 # contains the link to the Rietveld issue, while the Rietveld message contains
1545 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001546 # Keep a separate copy for the commit message.
1547 if cl.GetIssue():
1548 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1549
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001550 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001551 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001552 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001553 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001554 commit_desc.append_footer('Patch from %s.' % options.contributor)
1555
1556 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557
1558 branches = [base_branch, cl.GetBranchRef()]
1559 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001560 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001561 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001563 # We want to squash all this branch's commits into one commit with the proper
1564 # description. We do this by doing a "reset --soft" to the base branch (which
1565 # keeps the working copy the same), then dcommitting that. If origin/master
1566 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1567 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001568 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001569 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1570 # Delete the branches if they exist.
1571 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1572 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1573 result = RunGitWithCode(showref_cmd)
1574 if result[0] == 0:
1575 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576
1577 # We might be in a directory that's present in this branch but not in the
1578 # trunk. Move up to the top of the tree so that git commands that expect a
1579 # valid CWD won't fail after we check out the merge branch.
1580 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1581 if rel_base_path:
1582 os.chdir(rel_base_path)
1583
1584 # Stuff our change into the merge branch.
1585 # We wrap in a try...finally block so if anything goes wrong,
1586 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001587 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001589 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1590 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001592 RunGit(
1593 [
1594 'commit', '--author', options.contributor,
1595 '-m', commit_desc.description,
1596 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001598 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001599 if base_has_submodules:
1600 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1601 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1602 RunGit(['checkout', CHERRY_PICK_BRANCH])
1603 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 if cmd == 'push':
1605 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001606 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607 retcode, output = RunGitWithCode(
1608 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1609 logging.debug(output)
1610 else:
1611 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001612 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001613 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001614 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001615 finally:
1616 # And then swap back to the original branch and clean up.
1617 RunGit(['checkout', '-q', cl.GetBranch()])
1618 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001619 if base_has_submodules:
1620 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621
1622 if cl.GetIssue():
1623 if cmd == 'dcommit' and 'Committed r' in output:
1624 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1625 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001626 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1627 for l in output.splitlines(False))
1628 match = filter(None, match)
1629 if len(match) != 1:
1630 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1631 output)
1632 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001633 else:
1634 return 1
1635 viewvc_url = settings.GetViewVCUrl()
1636 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001637 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001638 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001639 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 print ('Closing issue '
1641 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001642 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001644 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001645 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001646 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001647 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1648 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001650
1651 if retcode == 0:
1652 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1653 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001654 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656 return 0
1657
1658
1659@usage('[upstream branch to apply against]')
1660def CMDdcommit(parser, args):
1661 """commit the current changelist via git-svn"""
1662 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001663 message = """This doesn't appear to be an SVN repository.
1664If your project has a git mirror with an upstream SVN master, you probably need
1665to run 'git svn init', see your project's git mirror documentation.
1666If your project has a true writeable upstream repository, you probably want
1667to run 'git cl push' instead.
1668Choose wisely, if you get this wrong, your commit might appear to succeed but
1669will instead be silently ignored."""
1670 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001671 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672 return SendUpstream(parser, args, 'dcommit')
1673
1674
1675@usage('[upstream branch to apply against]')
1676def CMDpush(parser, args):
1677 """commit the current changelist via git"""
1678 if settings.GetIsGitSvn():
1679 print('This appears to be an SVN repository.')
1680 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001681 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 return SendUpstream(parser, args, 'push')
1683
1684
1685@usage('<patch url or issue id>')
1686def CMDpatch(parser, args):
1687 """patch in a code review"""
1688 parser.add_option('-b', dest='newbranch',
1689 help='create a new branch off trunk for the patch')
1690 parser.add_option('-f', action='store_true', dest='force',
1691 help='with -b, clobber any existing branch')
1692 parser.add_option('--reject', action='store_true', dest='reject',
1693 help='allow failed patches and spew .rej files')
1694 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1695 help="don't commit after patch applies")
1696 (options, args) = parser.parse_args(args)
1697 if len(args) != 1:
1698 parser.print_help()
1699 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001700 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001702 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001703 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001704
maruel@chromium.org52424302012-08-29 15:14:30 +00001705 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001707 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001708 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001709 patchset = cl.GetMostRecentPatchset(issue)
1710 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001711 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001712 # Assume it's a URL to the patch. Default to https.
1713 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001714 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001715 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716 DieWithError('Must pass an issue ID or full URL for '
1717 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001718 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001719 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001720 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001721
1722 if options.newbranch:
1723 if options.force:
1724 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001725 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 RunGit(['checkout', '-b', options.newbranch,
1727 Changelist().GetUpstreamBranch()])
1728
1729 # Switch up to the top-level directory, if necessary, in preparation for
1730 # applying the patch.
1731 top = RunGit(['rev-parse', '--show-cdup']).strip()
1732 if top:
1733 os.chdir(top)
1734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 # Git patches have a/ at the beginning of source paths. We strip that out
1736 # with a sed script rather than the -p flag to patch so we can feed either
1737 # Git or svn-style patches into the same apply command.
1738 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001739 try:
1740 patch_data = subprocess2.check_output(
1741 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1742 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 DieWithError('Git patch mungling failed.')
1744 logging.info(patch_data)
1745 # We use "git apply" to apply the patch instead of "patch" so that we can
1746 # pick up file adds.
1747 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001748 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749 if options.reject:
1750 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001751 try:
1752 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1753 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754 DieWithError('Failed to apply the patch')
1755
1756 # If we had an issue, commit the current state and register the issue.
1757 if not options.nocommit:
1758 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1759 cl = Changelist()
1760 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001761 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001762 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763 else:
1764 print "Patch applied to index."
1765 return 0
1766
1767
1768def CMDrebase(parser, args):
1769 """rebase current branch on top of svn repo"""
1770 # Provide a wrapper for git svn rebase to help avoid accidental
1771 # git svn dcommit.
1772 # It's the only command that doesn't use parser at all since we just defer
1773 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001774 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001775
1776
1777def GetTreeStatus():
1778 """Fetches the tree status and returns either 'open', 'closed',
1779 'unknown' or 'unset'."""
1780 url = settings.GetTreeStatusUrl(error_ok=True)
1781 if url:
1782 status = urllib2.urlopen(url).read().lower()
1783 if status.find('closed') != -1 or status == '0':
1784 return 'closed'
1785 elif status.find('open') != -1 or status == '1':
1786 return 'open'
1787 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788 return 'unset'
1789
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001790
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791def GetTreeStatusReason():
1792 """Fetches the tree status from a json url and returns the message
1793 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001794 url = settings.GetTreeStatusUrl()
1795 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 connection = urllib2.urlopen(json_url)
1797 status = json.loads(connection.read())
1798 connection.close()
1799 return status['message']
1800
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001801
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001802def CMDtree(parser, args):
1803 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001804 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805 status = GetTreeStatus()
1806 if 'unset' == status:
1807 print 'You must configure your tree status URL by running "git cl config".'
1808 return 2
1809
1810 print "The tree is %s" % status
1811 print
1812 print GetTreeStatusReason()
1813 if status != 'open':
1814 return 1
1815 return 0
1816
1817
maruel@chromium.org15192402012-09-06 12:38:29 +00001818def CMDtry(parser, args):
1819 """Triggers a try job through Rietveld."""
1820 group = optparse.OptionGroup(parser, "Try job options")
1821 group.add_option(
1822 "-b", "--bot", action="append",
1823 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1824 "times to specify multiple builders. ex: "
1825 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1826 "the try server waterfall for the builders name and the tests "
1827 "available. Can also be used to specify gtest_filter, e.g. "
1828 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1829 group.add_option(
1830 "-r", "--revision",
1831 help="Revision to use for the try job; default: the "
1832 "revision will be determined by the try server; see "
1833 "its waterfall for more info")
1834 group.add_option(
1835 "-c", "--clobber", action="store_true", default=False,
1836 help="Force a clobber before building; e.g. don't do an "
1837 "incremental build")
1838 group.add_option(
1839 "--project",
1840 help="Override which project to use. Projects are defined "
1841 "server-side to define what default bot set to use")
1842 group.add_option(
1843 "-t", "--testfilter", action="append", default=[],
1844 help=("Apply a testfilter to all the selected builders. Unless the "
1845 "builders configurations are similar, use multiple "
1846 "--bot <builder>:<test> arguments."))
1847 group.add_option(
1848 "-n", "--name", help="Try job name; default to current branch name")
1849 parser.add_option_group(group)
1850 options, args = parser.parse_args(args)
1851
1852 if args:
1853 parser.error('Unknown arguments: %s' % args)
1854
1855 cl = Changelist()
1856 if not cl.GetIssue():
1857 parser.error('Need to upload first')
1858
1859 if not options.name:
1860 options.name = cl.GetBranch()
1861
1862 # Process --bot and --testfilter.
1863 if not options.bot:
1864 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001865 change = cl.GetChange(
1866 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1867 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001868 options.bot = presubmit_support.DoGetTrySlaves(
1869 change,
1870 change.LocalPaths(),
1871 settings.GetRoot(),
1872 None,
1873 None,
1874 options.verbose,
1875 sys.stdout)
1876 if not options.bot:
1877 parser.error('No default try builder to try, use --bot')
1878
1879 builders_and_tests = {}
1880 for bot in options.bot:
1881 if ':' in bot:
1882 builder, tests = bot.split(':', 1)
1883 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1884 elif ',' in bot:
1885 parser.error('Specify one bot per --bot flag')
1886 else:
1887 builders_and_tests.setdefault(bot, []).append('defaulttests')
1888
1889 if options.testfilter:
1890 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1891 builders_and_tests = dict(
1892 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1893 if t != ['compile'])
1894
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001895 if any('triggered' in b for b in builders_and_tests):
1896 print >> sys.stderr, (
1897 'ERROR You are trying to send a job to a triggered bot. This type of'
1898 ' bot requires an\ninitial job from a parent (usually a builder). '
1899 'Instead send your job to the parent.\n'
1900 'Bot list: %s' % builders_and_tests)
1901 return 1
1902
maruel@chromium.org15192402012-09-06 12:38:29 +00001903 patchset = cl.GetPatchset()
1904 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001905 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001906
1907 cl.RpcServer().trigger_try_jobs(
1908 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1909 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001910 print('Tried jobs on:')
1911 length = max(len(builder) for builder in builders_and_tests)
1912 for builder in sorted(builders_and_tests):
1913 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001914 return 0
1915
1916
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001917@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001919 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001920 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001921 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001922 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001923 return 0
1924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001926 if args:
1927 # One arg means set upstream branch.
1928 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1929 cl = Changelist()
1930 print "Upstream branch set to " + cl.GetUpstreamBranch()
1931 else:
1932 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 return 0
1934
1935
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001936def CMDset_commit(parser, args):
1937 """set the commit bit"""
1938 _, args = parser.parse_args(args)
1939 if args:
1940 parser.error('Unrecognized args: %s' % ' '.join(args))
1941 cl = Changelist()
1942 cl.SetFlag('commit', '1')
1943 return 0
1944
1945
groby@chromium.org411034a2013-02-26 15:12:01 +00001946def CMDset_close(parser, args):
1947 """close the issue"""
1948 _, args = parser.parse_args(args)
1949 if args:
1950 parser.error('Unrecognized args: %s' % ' '.join(args))
1951 cl = Changelist()
1952 # Ensure there actually is an issue to close.
1953 cl.GetDescription()
1954 cl.CloseIssue()
1955 return 0
1956
1957
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001958def CMDformat(parser, args):
1959 """run clang-format on the diff"""
1960 CLANG_EXTS = ['.cc', '.cpp', '.h']
1961 parser.add_option('--full', action='store_true', default=False)
1962 opts, args = parser.parse_args(args)
1963 if args:
1964 parser.error('Unrecognized args: %s' % ' '.join(args))
1965
digit@chromium.org29e47272013-05-17 17:01:46 +00001966 # Generate diff for the current branch's changes.
1967 diff_cmd = ['diff', '--no-ext-diff']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001968 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00001969 # Only list the names of modified files.
1970 diff_cmd.append('--name-only')
1971 else:
1972 # Only generate context-less patches.
1973 diff_cmd.append('-U0')
1974
1975 # Grab the merge-base commit, i.e. the upstream commit of the current
1976 # branch when it was created or the last time it was rebased. This is
1977 # to cover the case where the user may have called "git fetch origin",
1978 # moving the origin branch to a newer commit, but hasn't rebased yet.
1979 upstream_commit = None
1980 cl = Changelist()
1981 upstream_branch = cl.GetUpstreamBranch()
1982 if upstream_branch:
1983 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
1984 upstream_commit = upstream_commit.strip()
1985
1986 if not upstream_commit:
1987 DieWithError('Could not find base commit for this branch. '
1988 'Are you in detached state?')
1989
1990 diff_cmd.append(upstream_commit)
1991
1992 # Handle source file filtering.
1993 diff_cmd.append('--')
1994 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
1995 diff_output = RunGit(diff_cmd)
1996
1997 if opts.full:
1998 # diff_output is a list of files to send to clang-format.
1999 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002000 if not files:
2001 print "Nothing to format."
2002 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002003 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002004 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002005 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002006 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2007 'clang-format-diff.py')
2008 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002009 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2010 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2011 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002012
2013 return 0
2014
2015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016def Command(name):
2017 return getattr(sys.modules[__name__], 'CMD' + name, None)
2018
2019
2020def CMDhelp(parser, args):
2021 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002022 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 if len(args) == 1:
2024 return main(args + ['--help'])
2025 parser.print_help()
2026 return 0
2027
2028
2029def GenUsage(parser, command):
2030 """Modify an OptParse object with the function's documentation."""
2031 obj = Command(command)
2032 more = getattr(obj, 'usage_more', '')
2033 if command == 'help':
2034 command = '<command>'
2035 else:
2036 # OptParser.description prefer nicely non-formatted strings.
2037 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2038 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2039
2040
2041def main(argv):
2042 """Doesn't parse the arguments here, just find the right subcommand to
2043 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002044 if sys.hexversion < 0x02060000:
2045 print >> sys.stderr, (
2046 '\nYour python version %s is unsupported, please upgrade.\n' %
2047 sys.version.split(' ', 1)[0])
2048 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002049
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002050 # Reload settings.
2051 global settings
2052 settings = Settings()
2053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054 # Do it late so all commands are listed.
2055 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
2056 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
2057 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
2058
2059 # Create the option parse and add --verbose support.
2060 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002061 parser.add_option(
2062 '-v', '--verbose', action='count', default=0,
2063 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064 old_parser_args = parser.parse_args
2065 def Parse(args):
2066 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002067 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002069 elif options.verbose:
2070 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071 else:
2072 logging.basicConfig(level=logging.WARNING)
2073 return options, args
2074 parser.parse_args = Parse
2075
2076 if argv:
2077 command = Command(argv[0])
2078 if command:
2079 # "fix" the usage and the description now that we know the subcommand.
2080 GenUsage(parser, argv[0])
2081 try:
2082 return command(parser, argv[1:])
2083 except urllib2.HTTPError, e:
2084 if e.code != 500:
2085 raise
2086 DieWithError(
2087 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2088 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2089
2090 # Not a known command. Default to help.
2091 GenUsage(parser, 'help')
2092 return CMDhelp(parser, argv)
2093
2094
2095if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002096 # These affect sys.stdout so do it outside of main() to simplify mocks in
2097 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002098 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002099 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002100 sys.exit(main(sys.argv[1:]))