blob: a93a04b27ac2b1888b4f49520f52387ef0ef04c6 [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
27from third_party import upload
28import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000029import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000032import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000034import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import watchlists
36
37
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000038DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000039POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000041GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000042CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000043
maruel@chromium.org90541732011-04-01 17:54:18 +000044
maruel@chromium.orgddd59412011-11-30 14:20:38 +000045# Initialized in main()
46settings = None
47
48
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000050 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051 sys.exit(1)
52
53
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000056 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000057 except subprocess2.CalledProcessError as e:
58 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061 'Command "%s" failed.\n%s' % (
62 ' '.join(args), error_message or e.stdout or ''))
63 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000064
65
66def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000067 """Returns stdout."""
bratell@opera.comf267b0e2013-05-02 09:11:43 +000068 return RunCommand(['git', '--no-pager'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
70
71def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000073 try:
bratell@opera.comf267b0e2013-05-02 09:11:43 +000074 out, code = subprocess2.communicate(['git', '--no-pager'] + args,
75 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000076 return code, out[0]
77 except ValueError:
78 # When the subprocess fails, it returns None. That triggers a ValueError
79 # when trying to unpack the return value into (out, code).
80 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000081
82
83def usage(more):
84 def hook(fn):
85 fn.usage_more = more
86 return fn
87 return hook
88
89
maruel@chromium.org90541732011-04-01 17:54:18 +000090def ask_for_data(prompt):
91 try:
92 return raw_input(prompt)
93 except KeyboardInterrupt:
94 # Hide the exception.
95 sys.exit(1)
96
97
iannucci@chromium.org79540052012-10-19 23:15:26 +000098def git_set_branch_value(key, value):
99 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000100 if not branch:
101 return
102
103 cmd = ['config']
104 if isinstance(value, int):
105 cmd.append('--int')
106 git_key = 'branch.%s.%s' % (branch, key)
107 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000108
109
110def git_get_branch_default(key, default):
111 branch = Changelist().GetBranch()
112 if branch:
113 git_key = 'branch.%s.%s' % (branch, key)
114 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
115 try:
116 return int(stdout.strip())
117 except ValueError:
118 pass
119 return default
120
121
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000122def add_git_similarity(parser):
123 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000124 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000125 help='Sets the percentage that a pair of files need to match in order to'
126 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000127 parser.add_option(
128 '--find-copies', action='store_true',
129 help='Allows git to look for copies.')
130 parser.add_option(
131 '--no-find-copies', action='store_false', dest='find_copies',
132 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000133
134 old_parser_args = parser.parse_args
135 def Parse(args):
136 options, args = old_parser_args(args)
137
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000138 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000140 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000141 print('Note: Saving similarity of %d%% in git config.'
142 % options.similarity)
143 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000144
iannucci@chromium.org79540052012-10-19 23:15:26 +0000145 options.similarity = max(0, min(options.similarity, 100))
146
147 if options.find_copies is None:
148 options.find_copies = bool(
149 git_get_branch_default('git-find-copies', True))
150 else:
151 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152
153 print('Using %d%% similarity for rename/copy detection. '
154 'Override with --similarity.' % options.similarity)
155
156 return options, args
157 parser.parse_args = Parse
158
159
ukai@chromium.org259e4682012-10-25 07:36:33 +0000160def is_dirty_git_tree(cmd):
161 # Make sure index is up-to-date before running diff-index.
162 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
163 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
164 if dirty:
165 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
166 print 'Uncommitted files: (git diff-index --name-status HEAD)'
167 print dirty[:4096]
168 if len(dirty) > 4096:
169 print '... (run "git diff-index --name-status HEAD" to see full output).'
170 return True
171 return False
172
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000173
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000174def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
175 """Return the corresponding git ref if |base_url| together with |glob_spec|
176 matches the full |url|.
177
178 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
179 """
180 fetch_suburl, as_ref = glob_spec.split(':')
181 if allow_wildcards:
182 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
183 if glob_match:
184 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
185 # "branches/{472,597,648}/src:refs/remotes/svn/*".
186 branch_re = re.escape(base_url)
187 if glob_match.group(1):
188 branch_re += '/' + re.escape(glob_match.group(1))
189 wildcard = glob_match.group(2)
190 if wildcard == '*':
191 branch_re += '([^/]*)'
192 else:
193 # Escape and replace surrounding braces with parentheses and commas
194 # with pipe symbols.
195 wildcard = re.escape(wildcard)
196 wildcard = re.sub('^\\\\{', '(', wildcard)
197 wildcard = re.sub('\\\\,', '|', wildcard)
198 wildcard = re.sub('\\\\}$', ')', wildcard)
199 branch_re += wildcard
200 if glob_match.group(3):
201 branch_re += re.escape(glob_match.group(3))
202 match = re.match(branch_re, url)
203 if match:
204 return re.sub('\*$', match.group(1), as_ref)
205
206 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
207 if fetch_suburl:
208 full_url = base_url + '/' + fetch_suburl
209 else:
210 full_url = base_url
211 if full_url == url:
212 return as_ref
213 return None
214
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000215
iannucci@chromium.org79540052012-10-19 23:15:26 +0000216def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000217 """Prints statistics about the change to the user."""
218 # --no-ext-diff is broken in some versions of Git, so try to work around
219 # this by overriding the environment (but there is still a problem if the
220 # git config key "diff.external" is used).
221 env = os.environ.copy()
222 if 'GIT_EXTERNAL_DIFF' in env:
223 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000224
225 if find_copies:
226 similarity_options = ['--find-copies-harder', '-l100000',
227 '-C%s' % similarity]
228 else:
229 similarity_options = ['-M%s' % similarity]
230
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000231 return subprocess2.call(
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000232 ['git', '--no-pager',
233 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000234 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000235
236
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000237class Settings(object):
238 def __init__(self):
239 self.default_server = None
240 self.cc = None
241 self.root = None
242 self.is_git_svn = None
243 self.svn_branch = None
244 self.tree_status_url = None
245 self.viewvc_url = None
246 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000247 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000248 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000249
250 def LazyUpdateIfNeeded(self):
251 """Updates the settings from a codereview.settings file, if available."""
252 if not self.updated:
253 cr_settings_file = FindCodereviewSettingsFile()
254 if cr_settings_file:
255 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000256 self.updated = True
257 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000258 self.updated = True
259
260 def GetDefaultServerUrl(self, error_ok=False):
261 if not self.default_server:
262 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000263 self.default_server = gclient_utils.UpgradeToHttps(
264 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000265 if error_ok:
266 return self.default_server
267 if not self.default_server:
268 error_message = ('Could not find settings file. You must configure '
269 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000270 self.default_server = gclient_utils.UpgradeToHttps(
271 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272 return self.default_server
273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000274 def GetRoot(self):
275 if not self.root:
276 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
277 return self.root
278
279 def GetIsGitSvn(self):
280 """Return true if this repo looks like it's using git-svn."""
281 if self.is_git_svn is None:
282 # If you have any "svn-remote.*" config keys, we think you're using svn.
283 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000284 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285 return self.is_git_svn
286
287 def GetSVNBranch(self):
288 if self.svn_branch is None:
289 if not self.GetIsGitSvn():
290 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
291
292 # Try to figure out which remote branch we're based on.
293 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000294 # 1) iterate through our branch history and find the svn URL.
295 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296
297 # regexp matching the git-svn line that contains the URL.
298 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
299
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000300 # We don't want to go through all of history, so read a line from the
301 # pipe at a time.
302 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000303 cmd = ['git', '--no-pager', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000304 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000305 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000306 for line in proc.stdout:
307 match = git_svn_re.match(line)
308 if match:
309 url = match.group(1)
310 proc.stdout.close() # Cut pipe.
311 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000312
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000313 if url:
314 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
315 remotes = RunGit(['config', '--get-regexp',
316 r'^svn-remote\..*\.url']).splitlines()
317 for remote in remotes:
318 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000319 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000320 remote = match.group(1)
321 base_url = match.group(2)
322 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000323 ['config', 'svn-remote.%s.fetch' % remote],
324 error_ok=True).strip()
325 if fetch_spec:
326 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
327 if self.svn_branch:
328 break
329 branch_spec = RunGit(
330 ['config', 'svn-remote.%s.branches' % remote],
331 error_ok=True).strip()
332 if branch_spec:
333 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
334 if self.svn_branch:
335 break
336 tag_spec = RunGit(
337 ['config', 'svn-remote.%s.tags' % remote],
338 error_ok=True).strip()
339 if tag_spec:
340 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
341 if self.svn_branch:
342 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000343
344 if not self.svn_branch:
345 DieWithError('Can\'t guess svn branch -- try specifying it on the '
346 'command line')
347
348 return self.svn_branch
349
350 def GetTreeStatusUrl(self, error_ok=False):
351 if not self.tree_status_url:
352 error_message = ('You must configure your tree status URL by running '
353 '"git cl config".')
354 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
355 error_ok=error_ok,
356 error_message=error_message)
357 return self.tree_status_url
358
359 def GetViewVCUrl(self):
360 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000361 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000362 return self.viewvc_url
363
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000364 def GetDefaultCCList(self):
365 return self._GetConfig('rietveld.cc', error_ok=True)
366
ukai@chromium.orge8077812012-02-03 03:41:46 +0000367 def GetIsGerrit(self):
368 """Return true if this repo is assosiated with gerrit code review system."""
369 if self.is_gerrit is None:
370 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
371 return self.is_gerrit
372
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000373 def GetGitEditor(self):
374 """Return the editor specified in the git config, or None if none is."""
375 if self.git_editor is None:
376 self.git_editor = self._GetConfig('core.editor', error_ok=True)
377 return self.git_editor or None
378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000379 def _GetConfig(self, param, **kwargs):
380 self.LazyUpdateIfNeeded()
381 return RunGit(['config', param], **kwargs).strip()
382
383
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000384def ShortBranchName(branch):
385 """Convert a name like 'refs/heads/foo' to just 'foo'."""
386 return branch.replace('refs/heads/', '')
387
388
389class Changelist(object):
390 def __init__(self, branchref=None):
391 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000392 global settings
393 if not settings:
394 # Happens when git_cl.py is used as a utility library.
395 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000396 settings.GetDefaultServerUrl()
397 self.branchref = branchref
398 if self.branchref:
399 self.branch = ShortBranchName(self.branchref)
400 else:
401 self.branch = None
402 self.rietveld_server = None
403 self.upstream_branch = None
404 self.has_issue = False
405 self.issue = None
406 self.has_description = False
407 self.description = None
408 self.has_patchset = False
409 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000410 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000411 self.cc = None
412 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000413 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000414
415 def GetCCList(self):
416 """Return the users cc'd on this CL.
417
418 Return is a string suitable for passing to gcl with the --cc flag.
419 """
420 if self.cc is None:
421 base_cc = settings .GetDefaultCCList()
422 more_cc = ','.join(self.watchers)
423 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
424 return self.cc
425
426 def SetWatchers(self, watchers):
427 """Set the list of email addresses that should be cc'd based on the changed
428 files in this CL.
429 """
430 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431
432 def GetBranch(self):
433 """Returns the short branch name, e.g. 'master'."""
434 if not self.branch:
435 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
436 self.branch = ShortBranchName(self.branchref)
437 return self.branch
438
439 def GetBranchRef(self):
440 """Returns the full branch name, e.g. 'refs/heads/master'."""
441 self.GetBranch() # Poke the lazy loader.
442 return self.branchref
443
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000444 @staticmethod
445 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 """Returns a tuple containg remote and remote ref,
447 e.g. 'origin', 'refs/heads/master'
448 """
449 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000450 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
451 error_ok=True).strip()
452 if upstream_branch:
453 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
454 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000455 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
456 error_ok=True).strip()
457 if upstream_branch:
458 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000460 # Fall back on trying a git-svn upstream branch.
461 if settings.GetIsGitSvn():
462 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000463 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000464 # Else, try to guess the origin remote.
465 remote_branches = RunGit(['branch', '-r']).split()
466 if 'origin/master' in remote_branches:
467 # Fall back on origin/master if it exits.
468 remote = 'origin'
469 upstream_branch = 'refs/heads/master'
470 elif 'origin/trunk' in remote_branches:
471 # Fall back on origin/trunk if it exists. Generally a shared
472 # git-svn clone
473 remote = 'origin'
474 upstream_branch = 'refs/heads/trunk'
475 else:
476 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477Either pass complete "git diff"-style arguments, like
478 git cl upload origin/master
479or verify this branch is set up to track another (via the --track argument to
480"git checkout -b ...").""")
481
482 return remote, upstream_branch
483
484 def GetUpstreamBranch(self):
485 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000486 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000487 if remote is not '.':
488 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
489 self.upstream_branch = upstream_branch
490 return self.upstream_branch
491
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000492 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000493 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000494 remote, branch = None, self.GetBranch()
495 seen_branches = set()
496 while branch not in seen_branches:
497 seen_branches.add(branch)
498 remote, branch = self.FetchUpstreamTuple(branch)
499 branch = ShortBranchName(branch)
500 if remote != '.' or branch.startswith('refs/remotes'):
501 break
502 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000503 remotes = RunGit(['remote'], error_ok=True).split()
504 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000505 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000506 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000507 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000508 logging.warning('Could not determine which remote this change is '
509 'associated with, so defaulting to "%s". This may '
510 'not be what you want. You may prevent this message '
511 'by running "git svn info" as documented here: %s',
512 self._remote,
513 GIT_INSTRUCTIONS_URL)
514 else:
515 logging.warn('Could not determine which remote this change is '
516 'associated with. You may prevent this message by '
517 'running "git svn info" as documented here: %s',
518 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000519 branch = 'HEAD'
520 if branch.startswith('refs/remotes'):
521 self._remote = (remote, branch)
522 else:
523 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000524 return self._remote
525
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000526 def GitSanityChecks(self, upstream_git_obj):
527 """Checks git repo status and ensures diff is from local commits."""
528
529 # Verify the commit we're diffing against is in our current branch.
530 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
531 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
532 if upstream_sha != common_ancestor:
533 print >> sys.stderr, (
534 'ERROR: %s is not in the current branch. You may need to rebase '
535 'your tracking branch' % upstream_sha)
536 return False
537
538 # List the commits inside the diff, and verify they are all local.
539 commits_in_diff = RunGit(
540 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
541 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
542 remote_branch = remote_branch.strip()
543 if code != 0:
544 _, remote_branch = self.GetRemoteBranch()
545
546 commits_in_remote = RunGit(
547 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
548
549 common_commits = set(commits_in_diff) & set(commits_in_remote)
550 if common_commits:
551 print >> sys.stderr, (
552 'ERROR: Your diff contains %d commits already in %s.\n'
553 'Run "git log --oneline %s..HEAD" to get a list of commits in '
554 'the diff. If you are using a custom git flow, you can override'
555 ' the reference used for this check with "git config '
556 'gitcl.remotebranch <git-ref>".' % (
557 len(common_commits), remote_branch, upstream_git_obj))
558 return False
559 return True
560
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000561 def GetGitBaseUrlFromConfig(self):
562 """Return the configured base URL from branch.<branchname>.baseurl.
563
564 Returns None if it is not set.
565 """
566 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
567 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000568
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 def GetRemoteUrl(self):
570 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
571
572 Returns None if there is no remote.
573 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000574 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
576
577 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000578 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
581 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000582 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 else:
584 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.has_issue = True
586 return self.issue
587
588 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000589 if not self.rietveld_server:
590 # If we're on a branch then get the server potentially associated
591 # with that branch.
592 if self.GetIssue():
593 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
594 ['config', self._RietveldServer()], error_ok=True).strip())
595 if not self.rietveld_server:
596 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597 return self.rietveld_server
598
599 def GetIssueURL(self):
600 """Get the URL for a particular issue."""
601 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
602
603 def GetDescription(self, pretty=False):
604 if not self.has_description:
605 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000606 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000607 try:
608 self.description = self.RpcServer().get_description(issue).strip()
609 except urllib2.HTTPError, e:
610 if e.code == 404:
611 DieWithError(
612 ('\nWhile fetching the description for issue %d, received a '
613 '404 (not found)\n'
614 'error. It is likely that you deleted this '
615 'issue on the server. If this is the\n'
616 'case, please run\n\n'
617 ' git cl issue 0\n\n'
618 'to clear the association with the deleted issue. Then run '
619 'this command again.') % issue)
620 else:
621 DieWithError(
622 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 self.has_description = True
624 if pretty:
625 wrapper = textwrap.TextWrapper()
626 wrapper.initial_indent = wrapper.subsequent_indent = ' '
627 return wrapper.fill(self.description)
628 return self.description
629
630 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000631 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632 if not self.has_patchset:
633 patchset = RunGit(['config', self._PatchsetSetting()],
634 error_ok=True).strip()
635 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000636 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 else:
638 self.patchset = None
639 self.has_patchset = True
640 return self.patchset
641
642 def SetPatchset(self, patchset):
643 """Set this branch's patchset. If patchset=0, clears the patchset."""
644 if patchset:
645 RunGit(['config', self._PatchsetSetting(), str(patchset)])
646 else:
647 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000648 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 self.has_patchset = False
650
binji@chromium.org0281f522012-09-14 13:37:59 +0000651 def GetMostRecentPatchset(self, issue):
652 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000653 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000654
655 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000656 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000657 '/download/issue%s_%s.diff' % (issue, patchset))
658
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000659 def GetApprovingReviewers(self, issue):
660 return get_approving_reviewers(
661 self.RpcServer().get_issue_properties(int(issue), True))
662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000663 def SetIssue(self, issue):
664 """Set this branch's issue. If issue=0, clears the issue."""
665 if issue:
666 RunGit(['config', self._IssueSetting(), str(issue)])
667 if self.rietveld_server:
668 RunGit(['config', self._RietveldServer(), self.rietveld_server])
669 else:
670 RunGit(['config', '--unset', self._IssueSetting()])
671 self.SetPatchset(0)
672 self.has_issue = False
673
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000674 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000675 if not self.GitSanityChecks(upstream_branch):
676 DieWithError('\nGit sanity check failure')
677
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000678 root = RunCommand(['git', '--no-pager', 'rev-parse', '--show-cdup']).strip()
679 if not root:
680 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000681 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000682
683 # We use the sha1 of HEAD as a name of this change.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000684 name = RunCommand(['git', '--no-pager', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000685 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000686 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000687 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000688 except subprocess2.CalledProcessError:
689 DieWithError(
690 ('\nFailed to diff against upstream branch %s!\n\n'
691 'This branch probably doesn\'t exist anymore. To reset the\n'
692 'tracking branch, please run\n'
693 ' git branch --set-upstream %s trunk\n'
694 'replacing trunk with origin/master or the relevant branch') %
695 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000696
maruel@chromium.org52424302012-08-29 15:14:30 +0000697 issue = self.GetIssue()
698 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000699 if issue:
700 description = self.GetDescription()
701 else:
702 # If the change was never uploaded, use the log messages of all commits
703 # up to the branch point, as git cl upload will prefill the description
704 # with these log messages.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000705 description = RunCommand(['git', '--no-pager',
706 'log', '--pretty=format:%s%n%n%b',
maruel@chromium.org373af802012-05-25 21:07:33 +0000707 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000708
709 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000710 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000711 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000712 name,
713 description,
714 absroot,
715 files,
716 issue,
717 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000718 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000719
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000720 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000721 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000722
723 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000724 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000725 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000726 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000727 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000728 except presubmit_support.PresubmitFailure, e:
729 DieWithError(
730 ('%s\nMaybe your depot_tools is out of date?\n'
731 'If all fails, contact maruel@') % e)
732
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000733 def UpdateDescription(self, description):
734 self.description = description
735 return self.RpcServer().update_description(
736 self.GetIssue(), self.description)
737
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000739 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000740 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000742 def SetFlag(self, flag, value):
743 """Patchset must match."""
744 if not self.GetPatchset():
745 DieWithError('The patchset needs to match. Send another patchset.')
746 try:
747 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000748 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000749 except urllib2.HTTPError, e:
750 if e.code == 404:
751 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
752 if e.code == 403:
753 DieWithError(
754 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
755 'match?') % (self.GetIssue(), self.GetPatchset()))
756 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000758 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000759 """Returns an upload.RpcServer() to access this review's rietveld instance.
760 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000761 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000762 self._rpc_server = rietveld.CachingRietveld(
763 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000764 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765
766 def _IssueSetting(self):
767 """Return the git setting that stores this change's issue."""
768 return 'branch.%s.rietveldissue' % self.GetBranch()
769
770 def _PatchsetSetting(self):
771 """Return the git setting that stores this change's most recent patchset."""
772 return 'branch.%s.rietveldpatchset' % self.GetBranch()
773
774 def _RietveldServer(self):
775 """Returns the git setting that stores this change's rietveld server."""
776 return 'branch.%s.rietveldserver' % self.GetBranch()
777
778
779def GetCodereviewSettingsInteractively():
780 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000781 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 server = settings.GetDefaultServerUrl(error_ok=True)
783 prompt = 'Rietveld server (host[:port])'
784 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000785 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 if not server and not newserver:
787 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000788 if newserver:
789 newserver = gclient_utils.UpgradeToHttps(newserver)
790 if newserver != server:
791 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000793 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 prompt = caption
795 if initial:
796 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000797 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798 if new_val == 'x':
799 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000800 elif new_val:
801 if is_url:
802 new_val = gclient_utils.UpgradeToHttps(new_val)
803 if new_val != initial:
804 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000806 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000808 'tree-status-url', False)
809 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810
811 # TODO: configure a default branch to diff against, rather than this
812 # svn-based hackery.
813
814
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000815class ChangeDescription(object):
816 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000817 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000818
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000819 def __init__(self, description):
820 self._description = (description or '').strip()
821
822 @property
823 def description(self):
824 return self._description
825
826 def update_reviewers(self, reviewers):
827 """Rewrites the R=/TBR= line(s) as a single line."""
828 assert isinstance(reviewers, list), reviewers
829 if not reviewers:
830 return
831 regexp = re.compile(self.R_LINE, re.MULTILINE)
832 matches = list(regexp.finditer(self._description))
833 is_tbr = any(m.group(1) == 'TBR' for m in matches)
834 if len(matches) > 1:
835 # Erase all except the first one.
836 for i in xrange(len(matches) - 1, 0, -1):
837 self._description = (
838 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000839 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000840
841 if is_tbr:
842 new_r_line = 'TBR=' + ', '.join(reviewers)
843 else:
844 new_r_line = 'R=' + ', '.join(reviewers)
845
846 if matches:
847 self._description = (
848 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000849 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000850 else:
851 self.append_footer(new_r_line)
852
853 def prompt(self):
854 """Asks the user to update the description."""
855 self._description = (
856 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000857 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000858 '# The first line will also be used as the subject of the review.\n'
859 ) + self._description
860
861 if '\nBUG=' not in self._description:
862 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000863 content = gclient_utils.RunEditor(self._description, True,
864 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000865 if not content:
866 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000867
868 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000869 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000870 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000871 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000872 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000873
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000874 def append_footer(self, line):
875 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
876 if self._description:
877 if '\n' not in self._description:
878 self._description += '\n'
879 else:
880 last_line = self._description.rsplit('\n', 1)[1]
881 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
882 not presubmit_support.Change.TAG_LINE_RE.match(line)):
883 self._description += '\n'
884 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000885
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000886 def get_reviewers(self):
887 """Retrieves the list of reviewers."""
888 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000889 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000890 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000891
892
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000893def get_approving_reviewers(props):
894 """Retrieves the reviewers that approved a CL from the issue properties with
895 messages.
896
897 Note that the list may contain reviewers that are not committer, thus are not
898 considered by the CQ.
899 """
900 return sorted(
901 set(
902 message['sender']
903 for message in props['messages']
904 if message['approval'] and message['sender'] in props['reviewers']
905 )
906 )
907
908
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909def FindCodereviewSettingsFile(filename='codereview.settings'):
910 """Finds the given file starting in the cwd and going up.
911
912 Only looks up to the top of the repository unless an
913 'inherit-review-settings-ok' file exists in the root of the repository.
914 """
915 inherit_ok_file = 'inherit-review-settings-ok'
916 cwd = os.getcwd()
917 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
918 if os.path.isfile(os.path.join(root, inherit_ok_file)):
919 root = '/'
920 while True:
921 if filename in os.listdir(cwd):
922 if os.path.isfile(os.path.join(cwd, filename)):
923 return open(os.path.join(cwd, filename))
924 if cwd == root:
925 break
926 cwd = os.path.dirname(cwd)
927
928
929def LoadCodereviewSettingsFromFile(fileobj):
930 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000931 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933 def SetProperty(name, setting, unset_error_ok=False):
934 fullname = 'rietveld.' + name
935 if setting in keyvals:
936 RunGit(['config', fullname, keyvals[setting]])
937 else:
938 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
939
940 SetProperty('server', 'CODE_REVIEW_SERVER')
941 # Only server setting is required. Other settings can be absent.
942 # In that case, we ignore errors raised during option deletion attempt.
943 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
944 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
945 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
946
ukai@chromium.orge8077812012-02-03 03:41:46 +0000947 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
948 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
949 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
952 #should be of the form
953 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
954 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
955 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
956 keyvals['ORIGIN_URL_CONFIG']])
957
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000959def urlretrieve(source, destination):
960 """urllib is broken for SSL connections via a proxy therefore we
961 can't use urllib.urlretrieve()."""
962 with open(destination, 'w') as f:
963 f.write(urllib2.urlopen(source).read())
964
965
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000966def DownloadHooks(force):
967 """downloads hooks
968
969 Args:
970 force: True to update hooks. False to install hooks if not present.
971 """
972 if not settings.GetIsGerrit():
973 return
974 server_url = settings.GetDefaultServerUrl()
975 src = '%s/tools/hooks/commit-msg' % server_url
976 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
977 if not os.access(dst, os.X_OK):
978 if os.path.exists(dst):
979 if not force:
980 return
981 os.remove(dst)
982 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000983 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000984 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
985 except Exception:
986 if os.path.exists(dst):
987 os.remove(dst)
988 DieWithError('\nFailed to download hooks from %s' % src)
989
990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991@usage('[repo root containing codereview.settings]')
992def CMDconfig(parser, args):
993 """edit configuration for this tree"""
994
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000995 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 if len(args) == 0:
997 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000998 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 return 0
1000
1001 url = args[0]
1002 if not url.endswith('codereview.settings'):
1003 url = os.path.join(url, 'codereview.settings')
1004
1005 # Load code review settings and download hooks (if available).
1006 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001007 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 return 0
1009
1010
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001011def CMDbaseurl(parser, args):
1012 """get or set base-url for this branch"""
1013 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1014 branch = ShortBranchName(branchref)
1015 _, args = parser.parse_args(args)
1016 if not args:
1017 print("Current base-url:")
1018 return RunGit(['config', 'branch.%s.base-url' % branch],
1019 error_ok=False).strip()
1020 else:
1021 print("Setting base-url to %s" % args[0])
1022 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1023 error_ok=False).strip()
1024
1025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026def CMDstatus(parser, args):
1027 """show status of changelists"""
1028 parser.add_option('--field',
1029 help='print only specific field (desc|id|patch|url)')
1030 (options, args) = parser.parse_args(args)
1031
1032 # TODO: maybe make show_branches a flag if necessary.
1033 show_branches = not options.field
1034
1035 if show_branches:
1036 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1037 if branches:
1038 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001039 changes = (Changelist(branchref=b) for b in branches.splitlines())
1040 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1041 alignment = max(5, max(len(b) for b in branches))
1042 for branch in sorted(branches):
1043 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044
1045 cl = Changelist()
1046 if options.field:
1047 if options.field.startswith('desc'):
1048 print cl.GetDescription()
1049 elif options.field == 'id':
1050 issueid = cl.GetIssue()
1051 if issueid:
1052 print issueid
1053 elif options.field == 'patch':
1054 patchset = cl.GetPatchset()
1055 if patchset:
1056 print patchset
1057 elif options.field == 'url':
1058 url = cl.GetIssueURL()
1059 if url:
1060 print url
1061 else:
1062 print
1063 print 'Current branch:',
1064 if not cl.GetIssue():
1065 print 'no issue assigned.'
1066 return 0
1067 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001068 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069 print 'Issue description:'
1070 print cl.GetDescription(pretty=True)
1071 return 0
1072
1073
1074@usage('[issue_number]')
1075def CMDissue(parser, args):
1076 """Set or display the current code review issue number.
1077
1078 Pass issue number 0 to clear the current issue.
1079"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001080 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
1082 cl = Changelist()
1083 if len(args) > 0:
1084 try:
1085 issue = int(args[0])
1086 except ValueError:
1087 DieWithError('Pass a number to set the issue or none to list it.\n'
1088 'Maybe you want to run git cl status?')
1089 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001090 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 return 0
1092
1093
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001094def CMDcomments(parser, args):
1095 """show review comments of the current changelist"""
1096 (_, args) = parser.parse_args(args)
1097 if args:
1098 parser.error('Unsupported argument: %s' % args)
1099
1100 cl = Changelist()
1101 if cl.GetIssue():
1102 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1103 for message in sorted(data['messages'], key=lambda x: x['date']):
1104 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1105 if message['text'].strip():
1106 print '\n'.join(' ' + l for l in message['text'].splitlines())
1107 return 0
1108
1109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110def CreateDescriptionFromLog(args):
1111 """Pulls out the commit log to use as a base for the CL description."""
1112 log_args = []
1113 if len(args) == 1 and not args[0].endswith('.'):
1114 log_args = [args[0] + '..']
1115 elif len(args) == 1 and args[0].endswith('...'):
1116 log_args = [args[0][:-1]]
1117 elif len(args) == 2:
1118 log_args = [args[0] + '..' + args[1]]
1119 else:
1120 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001121 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122
1123
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124def CMDpresubmit(parser, args):
1125 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001126 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001128 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001129 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 (options, args) = parser.parse_args(args)
1131
ukai@chromium.org259e4682012-10-25 07:36:33 +00001132 if not options.force and is_dirty_git_tree('presubmit'):
1133 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 return 1
1135
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001136 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 if args:
1138 base_branch = args[0]
1139 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001140 # Default to diffing against the common ancestor of the upstream branch.
1141 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001143 cl.RunHook(
1144 committing=not options.upload,
1145 may_prompt=False,
1146 verbose=options.verbose,
1147 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001148 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149
1150
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001151def AddChangeIdToCommitMessage(options, args):
1152 """Re-commits using the current message, assumes the commit hook is in
1153 place.
1154 """
1155 log_desc = options.message or CreateDescriptionFromLog(args)
1156 git_command = ['commit', '--amend', '-m', log_desc]
1157 RunGit(git_command)
1158 new_log_desc = CreateDescriptionFromLog(args)
1159 if CHANGE_ID in new_log_desc:
1160 print 'git-cl: Added Change-Id to commit message.'
1161 else:
1162 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1163
1164
ukai@chromium.orge8077812012-02-03 03:41:46 +00001165def GerritUpload(options, args, cl):
1166 """upload the current branch to gerrit."""
1167 # We assume the remote called "origin" is the one we want.
1168 # It is probably not worthwhile to support different workflows.
1169 remote = 'origin'
1170 branch = 'master'
1171 if options.target_branch:
1172 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001174 change_desc = ChangeDescription(
1175 options.message or CreateDescriptionFromLog(args))
1176 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001177 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001179 if CHANGE_ID not in change_desc.description:
1180 AddChangeIdToCommitMessage(options, args)
1181 if options.reviewers:
1182 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183
ukai@chromium.orge8077812012-02-03 03:41:46 +00001184 receive_options = []
1185 cc = cl.GetCCList().split(',')
1186 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001187 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001188 cc = filter(None, cc)
1189 if cc:
1190 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001191 if change_desc.get_reviewers():
1192 receive_options.extend(
1193 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194
ukai@chromium.orge8077812012-02-03 03:41:46 +00001195 git_command = ['push']
1196 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001197 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001198 ' '.join(receive_options))
1199 git_command += [remote, 'HEAD:refs/for/' + branch]
1200 RunGit(git_command)
1201 # TODO(ukai): parse Change-Id: and set issue number?
1202 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001203
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204
ukai@chromium.orge8077812012-02-03 03:41:46 +00001205def RietveldUpload(options, args, cl):
1206 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1208 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 if options.emulate_svn_auto_props:
1210 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211
1212 change_desc = None
1213
1214 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001215 if options.title:
1216 upload_args.extend(['--title', options.title])
1217 elif options.message:
1218 # TODO(rogerta): for now, the -m option will also set the --title option
1219 # for upload.py. Soon this will be changed to set the --message option.
1220 # Will wait until people are used to typing -t instead of -m.
1221 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001222 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 print ("This branch is associated with issue %s. "
1224 "Adding patch to that issue." % cl.GetIssue())
1225 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001226 if options.title:
1227 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001228 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001229 change_desc = ChangeDescription(message)
1230 if options.reviewers:
1231 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001232 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001233 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001234
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001235 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 print "Description is empty; aborting."
1237 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001238
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001239 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001240 if change_desc.get_reviewers():
1241 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001242 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001243 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001244 DieWithError("Must specify reviewers to send email.")
1245 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001246 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001247 if cc:
1248 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001250 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001251 if not options.find_copies:
1252 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 # Include the upstream repo's URL in the change -- this is useful for
1255 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001256 remote_url = cl.GetGitBaseUrlFromConfig()
1257 if not remote_url:
1258 if settings.GetIsGitSvn():
1259 # URL is dependent on the current directory.
1260 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1261 if data:
1262 keys = dict(line.split(': ', 1) for line in data.splitlines()
1263 if ': ' in line)
1264 remote_url = keys.get('URL', None)
1265 else:
1266 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1267 remote_url = (cl.GetRemoteUrl() + '@'
1268 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 if remote_url:
1270 upload_args.extend(['--base_url', remote_url])
1271
1272 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001273 upload_args = ['upload'] + upload_args + args
1274 logging.info('upload.RealMain(%s)', upload_args)
1275 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001276 except KeyboardInterrupt:
1277 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 except:
1279 # If we got an exception after the user typed a description for their
1280 # change, back up the description before re-raising.
1281 if change_desc:
1282 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1283 print '\nGot exception while uploading -- saving description to %s\n' \
1284 % backup_path
1285 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001286 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 backup_file.close()
1288 raise
1289
1290 if not cl.GetIssue():
1291 cl.SetIssue(issue)
1292 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001293
1294 if options.use_commit_queue:
1295 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 return 0
1297
1298
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001299def cleanup_list(l):
1300 """Fixes a list so that comma separated items are put as individual items.
1301
1302 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1303 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1304 """
1305 items = sum((i.split(',') for i in l), [])
1306 stripped_items = (i.strip() for i in items)
1307 return sorted(filter(None, stripped_items))
1308
1309
ukai@chromium.orge8077812012-02-03 03:41:46 +00001310@usage('[args to "git diff"]')
1311def CMDupload(parser, args):
1312 """upload the current changelist to codereview"""
1313 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1314 help='bypass upload presubmit hook')
1315 parser.add_option('-f', action='store_true', dest='force',
1316 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001317 parser.add_option('-m', dest='message', help='message for patchset')
1318 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001319 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001320 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001321 help='reviewer email addresses')
1322 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001323 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001324 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001325 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001326 help='send email to reviewer immediately')
1327 parser.add_option("--emulate_svn_auto_props", action="store_true",
1328 dest="emulate_svn_auto_props",
1329 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001330 parser.add_option('-c', '--use-commit-queue', action='store_true',
1331 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001332 parser.add_option('--target_branch',
1333 help='When uploading to gerrit, remote branch to '
1334 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001335 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001336 (options, args) = parser.parse_args(args)
1337
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001338 if options.target_branch and not settings.GetIsGerrit():
1339 parser.error('Use --target_branch for non gerrit repository.')
1340
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001341 # Print warning if the user used the -m/--message argument. This will soon
1342 # change to -t/--title.
1343 if options.message:
1344 print >> sys.stderr, (
1345 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1346 'In the near future, -m or --message will send a message instead.\n'
1347 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001348
ukai@chromium.org259e4682012-10-25 07:36:33 +00001349 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001350 return 1
1351
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001352 options.reviewers = cleanup_list(options.reviewers)
1353 options.cc = cleanup_list(options.cc)
1354
ukai@chromium.orge8077812012-02-03 03:41:46 +00001355 cl = Changelist()
1356 if args:
1357 # TODO(ukai): is it ok for gerrit case?
1358 base_branch = args[0]
1359 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001360 # Default to diffing against common ancestor of upstream branch
1361 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001362 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001363
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001364 # Apply watchlists on upload.
1365 change = cl.GetChange(base_branch, None)
1366 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1367 files = [f.LocalPath() for f in change.AffectedFiles()]
1368 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1369
ukai@chromium.orge8077812012-02-03 03:41:46 +00001370 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001371 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001372 may_prompt=not options.force,
1373 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001374 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001375 if not hook_results.should_continue():
1376 return 1
1377 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001378 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001379
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001380 if cl.GetIssue():
1381 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1382 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001383 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001384 print ('The last upload made from this repository was patchset #%d but '
1385 'the most recent patchset on the server is #%d.'
1386 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001387 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1388 'from another machine or branch the patch you\'re uploading now '
1389 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001390 ask_for_data('About to upload; enter to confirm.')
1391
iannucci@chromium.org79540052012-10-19 23:15:26 +00001392 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001393 if settings.GetIsGerrit():
1394 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001395 ret = RietveldUpload(options, args, cl)
1396 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001397 git_set_branch_value('last-upload-hash',
1398 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001399
1400 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001401
1402
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001403def IsSubmoduleMergeCommit(ref):
1404 # When submodules are added to the repo, we expect there to be a single
1405 # non-git-svn merge commit at remote HEAD with a signature comment.
1406 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001407 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001408 return RunGit(cmd) != ''
1409
1410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411def SendUpstream(parser, args, cmd):
1412 """Common code for CmdPush and CmdDCommit
1413
1414 Squashed commit into a single.
1415 Updates changelog with metadata (e.g. pointer to review).
1416 Pushes/dcommits the code upstream.
1417 Updates review and closes.
1418 """
1419 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1420 help='bypass upload presubmit hook')
1421 parser.add_option('-m', dest='message',
1422 help="override review description")
1423 parser.add_option('-f', action='store_true', dest='force',
1424 help="force yes to questions (don't prompt)")
1425 parser.add_option('-c', dest='contributor',
1426 help="external contributor for patch (appended to " +
1427 "description and used as author for git). Should be " +
1428 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001429 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 (options, args) = parser.parse_args(args)
1431 cl = Changelist()
1432
1433 if not args or cmd == 'push':
1434 # Default to merging against our best guess of the upstream branch.
1435 args = [cl.GetUpstreamBranch()]
1436
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001437 if options.contributor:
1438 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1439 print "Please provide contibutor as 'First Last <email@example.com>'"
1440 return 1
1441
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001443 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444
ukai@chromium.org259e4682012-10-25 07:36:33 +00001445 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 return 1
1447
1448 # This rev-list syntax means "show all commits not in my branch that
1449 # are in base_branch".
1450 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1451 base_branch]).splitlines()
1452 if upstream_commits:
1453 print ('Base branch "%s" has %d commits '
1454 'not in this branch.' % (base_branch, len(upstream_commits)))
1455 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1456 return 1
1457
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001458 # This is the revision `svn dcommit` will commit on top of.
1459 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1460 '--pretty=format:%H'])
1461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001463 # If the base_head is a submodule merge commit, the first parent of the
1464 # base_head should be a git-svn commit, which is what we're interested in.
1465 base_svn_head = base_branch
1466 if base_has_submodules:
1467 base_svn_head += '^1'
1468
1469 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 if extra_commits:
1471 print ('This branch has %d additional commits not upstreamed yet.'
1472 % len(extra_commits.splitlines()))
1473 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1474 'before attempting to %s.' % (base_branch, cmd))
1475 return 1
1476
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001477 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001478 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001479 author = None
1480 if options.contributor:
1481 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001482 hook_results = cl.RunHook(
1483 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001484 may_prompt=not options.force,
1485 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001486 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001487 if not hook_results.should_continue():
1488 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489
1490 if cmd == 'dcommit':
1491 # Check the tree status if the tree status URL is set.
1492 status = GetTreeStatus()
1493 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001494 print('The tree is closed. Please wait for it to reopen. Use '
1495 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496 return 1
1497 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001498 print('Unable to determine tree status. Please verify manually and '
1499 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001500 else:
1501 breakpad.SendStack(
1502 'GitClHooksBypassedCommit',
1503 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001504 (cl.GetRietveldServer(), cl.GetIssue()),
1505 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001507 change_desc = ChangeDescription(options.message)
1508 if not change_desc.description and cl.GetIssue():
1509 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001511 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001512 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001513 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001514 else:
1515 print 'No description set.'
1516 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1517 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001519 # Keep a separate copy for the commit message, because the commit message
1520 # contains the link to the Rietveld issue, while the Rietveld message contains
1521 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001522 # Keep a separate copy for the commit message.
1523 if cl.GetIssue():
1524 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1525
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001526 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001527 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001528 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001530 commit_desc.append_footer('Patch from %s.' % options.contributor)
1531
1532 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533
1534 branches = [base_branch, cl.GetBranchRef()]
1535 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001536 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001537 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001538
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001539 # We want to squash all this branch's commits into one commit with the proper
1540 # description. We do this by doing a "reset --soft" to the base branch (which
1541 # keeps the working copy the same), then dcommitting that. If origin/master
1542 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1543 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001545 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1546 # Delete the branches if they exist.
1547 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1548 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1549 result = RunGitWithCode(showref_cmd)
1550 if result[0] == 0:
1551 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552
1553 # We might be in a directory that's present in this branch but not in the
1554 # trunk. Move up to the top of the tree so that git commands that expect a
1555 # valid CWD won't fail after we check out the merge branch.
1556 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1557 if rel_base_path:
1558 os.chdir(rel_base_path)
1559
1560 # Stuff our change into the merge branch.
1561 # We wrap in a try...finally block so if anything goes wrong,
1562 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001563 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001565 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1566 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001568 RunGit(
1569 [
1570 'commit', '--author', options.contributor,
1571 '-m', commit_desc.description,
1572 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001573 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001574 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001575 if base_has_submodules:
1576 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1577 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1578 RunGit(['checkout', CHERRY_PICK_BRANCH])
1579 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 if cmd == 'push':
1581 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001582 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583 retcode, output = RunGitWithCode(
1584 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1585 logging.debug(output)
1586 else:
1587 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001588 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001589 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001590 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591 finally:
1592 # And then swap back to the original branch and clean up.
1593 RunGit(['checkout', '-q', cl.GetBranch()])
1594 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001595 if base_has_submodules:
1596 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597
1598 if cl.GetIssue():
1599 if cmd == 'dcommit' and 'Committed r' in output:
1600 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1601 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001602 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1603 for l in output.splitlines(False))
1604 match = filter(None, match)
1605 if len(match) != 1:
1606 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1607 output)
1608 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 else:
1610 return 1
1611 viewvc_url = settings.GetViewVCUrl()
1612 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001613 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001614 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001615 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616 print ('Closing issue '
1617 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001618 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001620 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001621 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001622 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001623 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1624 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001625 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001626
1627 if retcode == 0:
1628 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1629 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001630 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632 return 0
1633
1634
1635@usage('[upstream branch to apply against]')
1636def CMDdcommit(parser, args):
1637 """commit the current changelist via git-svn"""
1638 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001639 message = """This doesn't appear to be an SVN repository.
1640If your project has a git mirror with an upstream SVN master, you probably need
1641to run 'git svn init', see your project's git mirror documentation.
1642If your project has a true writeable upstream repository, you probably want
1643to run 'git cl push' instead.
1644Choose wisely, if you get this wrong, your commit might appear to succeed but
1645will instead be silently ignored."""
1646 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001647 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001648 return SendUpstream(parser, args, 'dcommit')
1649
1650
1651@usage('[upstream branch to apply against]')
1652def CMDpush(parser, args):
1653 """commit the current changelist via git"""
1654 if settings.GetIsGitSvn():
1655 print('This appears to be an SVN repository.')
1656 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001657 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001658 return SendUpstream(parser, args, 'push')
1659
1660
1661@usage('<patch url or issue id>')
1662def CMDpatch(parser, args):
1663 """patch in a code review"""
1664 parser.add_option('-b', dest='newbranch',
1665 help='create a new branch off trunk for the patch')
1666 parser.add_option('-f', action='store_true', dest='force',
1667 help='with -b, clobber any existing branch')
1668 parser.add_option('--reject', action='store_true', dest='reject',
1669 help='allow failed patches and spew .rej files')
1670 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1671 help="don't commit after patch applies")
1672 (options, args) = parser.parse_args(args)
1673 if len(args) != 1:
1674 parser.print_help()
1675 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001676 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001678 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001679 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001680
maruel@chromium.org52424302012-08-29 15:14:30 +00001681 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001683 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001684 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001685 patchset = cl.GetMostRecentPatchset(issue)
1686 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001687 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001688 # Assume it's a URL to the patch. Default to https.
1689 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001690 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001691 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692 DieWithError('Must pass an issue ID or full URL for '
1693 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001694 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001695 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001696 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001697
1698 if options.newbranch:
1699 if options.force:
1700 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001701 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001702 RunGit(['checkout', '-b', options.newbranch,
1703 Changelist().GetUpstreamBranch()])
1704
1705 # Switch up to the top-level directory, if necessary, in preparation for
1706 # applying the patch.
1707 top = RunGit(['rev-parse', '--show-cdup']).strip()
1708 if top:
1709 os.chdir(top)
1710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001711 # Git patches have a/ at the beginning of source paths. We strip that out
1712 # with a sed script rather than the -p flag to patch so we can feed either
1713 # Git or svn-style patches into the same apply command.
1714 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001715 try:
1716 patch_data = subprocess2.check_output(
1717 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1718 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001719 DieWithError('Git patch mungling failed.')
1720 logging.info(patch_data)
1721 # We use "git apply" to apply the patch instead of "patch" so that we can
1722 # pick up file adds.
1723 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001724 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 if options.reject:
1726 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001727 try:
1728 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1729 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730 DieWithError('Failed to apply the patch')
1731
1732 # If we had an issue, commit the current state and register the issue.
1733 if not options.nocommit:
1734 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1735 cl = Changelist()
1736 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001737 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001738 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739 else:
1740 print "Patch applied to index."
1741 return 0
1742
1743
1744def CMDrebase(parser, args):
1745 """rebase current branch on top of svn repo"""
1746 # Provide a wrapper for git svn rebase to help avoid accidental
1747 # git svn dcommit.
1748 # It's the only command that doesn't use parser at all since we just defer
1749 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001750 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751
1752
1753def GetTreeStatus():
1754 """Fetches the tree status and returns either 'open', 'closed',
1755 'unknown' or 'unset'."""
1756 url = settings.GetTreeStatusUrl(error_ok=True)
1757 if url:
1758 status = urllib2.urlopen(url).read().lower()
1759 if status.find('closed') != -1 or status == '0':
1760 return 'closed'
1761 elif status.find('open') != -1 or status == '1':
1762 return 'open'
1763 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764 return 'unset'
1765
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767def GetTreeStatusReason():
1768 """Fetches the tree status from a json url and returns the message
1769 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001770 url = settings.GetTreeStatusUrl()
1771 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 connection = urllib2.urlopen(json_url)
1773 status = json.loads(connection.read())
1774 connection.close()
1775 return status['message']
1776
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001778def CMDtree(parser, args):
1779 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001780 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781 status = GetTreeStatus()
1782 if 'unset' == status:
1783 print 'You must configure your tree status URL by running "git cl config".'
1784 return 2
1785
1786 print "The tree is %s" % status
1787 print
1788 print GetTreeStatusReason()
1789 if status != 'open':
1790 return 1
1791 return 0
1792
1793
maruel@chromium.org15192402012-09-06 12:38:29 +00001794def CMDtry(parser, args):
1795 """Triggers a try job through Rietveld."""
1796 group = optparse.OptionGroup(parser, "Try job options")
1797 group.add_option(
1798 "-b", "--bot", action="append",
1799 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1800 "times to specify multiple builders. ex: "
1801 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1802 "the try server waterfall for the builders name and the tests "
1803 "available. Can also be used to specify gtest_filter, e.g. "
1804 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1805 group.add_option(
1806 "-r", "--revision",
1807 help="Revision to use for the try job; default: the "
1808 "revision will be determined by the try server; see "
1809 "its waterfall for more info")
1810 group.add_option(
1811 "-c", "--clobber", action="store_true", default=False,
1812 help="Force a clobber before building; e.g. don't do an "
1813 "incremental build")
1814 group.add_option(
1815 "--project",
1816 help="Override which project to use. Projects are defined "
1817 "server-side to define what default bot set to use")
1818 group.add_option(
1819 "-t", "--testfilter", action="append", default=[],
1820 help=("Apply a testfilter to all the selected builders. Unless the "
1821 "builders configurations are similar, use multiple "
1822 "--bot <builder>:<test> arguments."))
1823 group.add_option(
1824 "-n", "--name", help="Try job name; default to current branch name")
1825 parser.add_option_group(group)
1826 options, args = parser.parse_args(args)
1827
1828 if args:
1829 parser.error('Unknown arguments: %s' % args)
1830
1831 cl = Changelist()
1832 if not cl.GetIssue():
1833 parser.error('Need to upload first')
1834
1835 if not options.name:
1836 options.name = cl.GetBranch()
1837
1838 # Process --bot and --testfilter.
1839 if not options.bot:
1840 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001841 change = cl.GetChange(
1842 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1843 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001844 options.bot = presubmit_support.DoGetTrySlaves(
1845 change,
1846 change.LocalPaths(),
1847 settings.GetRoot(),
1848 None,
1849 None,
1850 options.verbose,
1851 sys.stdout)
1852 if not options.bot:
1853 parser.error('No default try builder to try, use --bot')
1854
1855 builders_and_tests = {}
1856 for bot in options.bot:
1857 if ':' in bot:
1858 builder, tests = bot.split(':', 1)
1859 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1860 elif ',' in bot:
1861 parser.error('Specify one bot per --bot flag')
1862 else:
1863 builders_and_tests.setdefault(bot, []).append('defaulttests')
1864
1865 if options.testfilter:
1866 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1867 builders_and_tests = dict(
1868 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1869 if t != ['compile'])
1870
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001871 if any('triggered' in b for b in builders_and_tests):
1872 print >> sys.stderr, (
1873 'ERROR You are trying to send a job to a triggered bot. This type of'
1874 ' bot requires an\ninitial job from a parent (usually a builder). '
1875 'Instead send your job to the parent.\n'
1876 'Bot list: %s' % builders_and_tests)
1877 return 1
1878
maruel@chromium.org15192402012-09-06 12:38:29 +00001879 patchset = cl.GetPatchset()
1880 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001881 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001882
1883 cl.RpcServer().trigger_try_jobs(
1884 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1885 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001886 print('Tried jobs on:')
1887 length = max(len(builder) for builder in builders_and_tests)
1888 for builder in sorted(builders_and_tests):
1889 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001890 return 0
1891
1892
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001893@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001895 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001896 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001897 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001898 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001899 return 0
1900
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001902 if args:
1903 # One arg means set upstream branch.
1904 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1905 cl = Changelist()
1906 print "Upstream branch set to " + cl.GetUpstreamBranch()
1907 else:
1908 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 return 0
1910
1911
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001912def CMDset_commit(parser, args):
1913 """set the commit bit"""
1914 _, args = parser.parse_args(args)
1915 if args:
1916 parser.error('Unrecognized args: %s' % ' '.join(args))
1917 cl = Changelist()
1918 cl.SetFlag('commit', '1')
1919 return 0
1920
1921
groby@chromium.org411034a2013-02-26 15:12:01 +00001922def CMDset_close(parser, args):
1923 """close the issue"""
1924 _, args = parser.parse_args(args)
1925 if args:
1926 parser.error('Unrecognized args: %s' % ' '.join(args))
1927 cl = Changelist()
1928 # Ensure there actually is an issue to close.
1929 cl.GetDescription()
1930 cl.CloseIssue()
1931 return 0
1932
1933
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001934def CMDformat(parser, args):
1935 """run clang-format on the diff"""
1936 CLANG_EXTS = ['.cc', '.cpp', '.h']
1937 parser.add_option('--full', action='store_true', default=False)
1938 opts, args = parser.parse_args(args)
1939 if args:
1940 parser.error('Unrecognized args: %s' % ' '.join(args))
1941
1942 if opts.full:
1943 cmd = ['diff', '--name-only', '--'] + ['.*' + ext for ext in CLANG_EXTS]
1944 files = RunGit(cmd).split()
1945 if not files:
1946 print "Nothing to format."
1947 return 0
1948 RunCommand(['clang-format', '-i'] + files)
1949 else:
1950 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
1951 'clang-format-diff.py')
1952 if not os.path.exists(cfd_path):
1953 print >> sys.stderr, 'Could not find clang-format-diff at %s.' % cfd_path
1954 return 2
1955 cmd = ['diff', '-U0', '@{u}', '--'] + ['.*' + ext for ext in CLANG_EXTS]
1956 diff = RunGit(cmd)
1957 cmd = [sys.executable, '/usr/lib/clang-format/clang-format-diff.py',
1958 '-style', 'Chromium']
1959 RunCommand(cmd, stdin=diff)
1960
1961 return 0
1962
1963
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964def Command(name):
1965 return getattr(sys.modules[__name__], 'CMD' + name, None)
1966
1967
1968def CMDhelp(parser, args):
1969 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001970 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971 if len(args) == 1:
1972 return main(args + ['--help'])
1973 parser.print_help()
1974 return 0
1975
1976
1977def GenUsage(parser, command):
1978 """Modify an OptParse object with the function's documentation."""
1979 obj = Command(command)
1980 more = getattr(obj, 'usage_more', '')
1981 if command == 'help':
1982 command = '<command>'
1983 else:
1984 # OptParser.description prefer nicely non-formatted strings.
1985 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1986 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1987
1988
1989def main(argv):
1990 """Doesn't parse the arguments here, just find the right subcommand to
1991 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001992 if sys.hexversion < 0x02060000:
1993 print >> sys.stderr, (
1994 '\nYour python version %s is unsupported, please upgrade.\n' %
1995 sys.version.split(' ', 1)[0])
1996 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001997 # Reload settings.
1998 global settings
1999 settings = Settings()
2000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001 # Do it late so all commands are listed.
2002 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
2003 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
2004 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
2005
2006 # Create the option parse and add --verbose support.
2007 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002008 parser.add_option(
2009 '-v', '--verbose', action='count', default=0,
2010 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 old_parser_args = parser.parse_args
2012 def Parse(args):
2013 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002014 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002016 elif options.verbose:
2017 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018 else:
2019 logging.basicConfig(level=logging.WARNING)
2020 return options, args
2021 parser.parse_args = Parse
2022
2023 if argv:
2024 command = Command(argv[0])
2025 if command:
2026 # "fix" the usage and the description now that we know the subcommand.
2027 GenUsage(parser, argv[0])
2028 try:
2029 return command(parser, argv[1:])
2030 except urllib2.HTTPError, e:
2031 if e.code != 500:
2032 raise
2033 DieWithError(
2034 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2035 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2036
2037 # Not a known command. Default to help.
2038 GenUsage(parser, 'help')
2039 return CMDhelp(parser, argv)
2040
2041
2042if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002043 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002044 sys.exit(main(sys.argv[1:]))