blob: 4e89e065e73a1d8a2b292a8b3361f0d50a6006ba [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000248
249 def LazyUpdateIfNeeded(self):
250 """Updates the settings from a codereview.settings file, if available."""
251 if not self.updated:
252 cr_settings_file = FindCodereviewSettingsFile()
253 if cr_settings_file:
254 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000255 self.updated = True
256 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000257 self.updated = True
258
259 def GetDefaultServerUrl(self, error_ok=False):
260 if not self.default_server:
261 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000262 self.default_server = gclient_utils.UpgradeToHttps(
263 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000264 if error_ok:
265 return self.default_server
266 if not self.default_server:
267 error_message = ('Could not find settings file. You must configure '
268 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000269 self.default_server = gclient_utils.UpgradeToHttps(
270 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000271 return self.default_server
272
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000273 def GetRoot(self):
274 if not self.root:
275 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
276 return self.root
277
278 def GetIsGitSvn(self):
279 """Return true if this repo looks like it's using git-svn."""
280 if self.is_git_svn is None:
281 # If you have any "svn-remote.*" config keys, we think you're using svn.
282 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000283 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000284 return self.is_git_svn
285
286 def GetSVNBranch(self):
287 if self.svn_branch is None:
288 if not self.GetIsGitSvn():
289 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
290
291 # Try to figure out which remote branch we're based on.
292 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000293 # 1) iterate through our branch history and find the svn URL.
294 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295
296 # regexp matching the git-svn line that contains the URL.
297 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
298
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000299 # We don't want to go through all of history, so read a line from the
300 # pipe at a time.
301 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000302 cmd = ['git', '--no-pager', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000303 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000304 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000305 for line in proc.stdout:
306 match = git_svn_re.match(line)
307 if match:
308 url = match.group(1)
309 proc.stdout.close() # Cut pipe.
310 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000311
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000312 if url:
313 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
314 remotes = RunGit(['config', '--get-regexp',
315 r'^svn-remote\..*\.url']).splitlines()
316 for remote in remotes:
317 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000318 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000319 remote = match.group(1)
320 base_url = match.group(2)
321 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000322 ['config', 'svn-remote.%s.fetch' % remote],
323 error_ok=True).strip()
324 if fetch_spec:
325 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
326 if self.svn_branch:
327 break
328 branch_spec = RunGit(
329 ['config', 'svn-remote.%s.branches' % remote],
330 error_ok=True).strip()
331 if branch_spec:
332 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
333 if self.svn_branch:
334 break
335 tag_spec = RunGit(
336 ['config', 'svn-remote.%s.tags' % remote],
337 error_ok=True).strip()
338 if tag_spec:
339 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
340 if self.svn_branch:
341 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000342
343 if not self.svn_branch:
344 DieWithError('Can\'t guess svn branch -- try specifying it on the '
345 'command line')
346
347 return self.svn_branch
348
349 def GetTreeStatusUrl(self, error_ok=False):
350 if not self.tree_status_url:
351 error_message = ('You must configure your tree status URL by running '
352 '"git cl config".')
353 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
354 error_ok=error_ok,
355 error_message=error_message)
356 return self.tree_status_url
357
358 def GetViewVCUrl(self):
359 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000360 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000361 return self.viewvc_url
362
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000363 def GetDefaultCCList(self):
364 return self._GetConfig('rietveld.cc', error_ok=True)
365
ukai@chromium.orge8077812012-02-03 03:41:46 +0000366 def GetIsGerrit(self):
367 """Return true if this repo is assosiated with gerrit code review system."""
368 if self.is_gerrit is None:
369 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
370 return self.is_gerrit
371
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372 def _GetConfig(self, param, **kwargs):
373 self.LazyUpdateIfNeeded()
374 return RunGit(['config', param], **kwargs).strip()
375
376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000377def ShortBranchName(branch):
378 """Convert a name like 'refs/heads/foo' to just 'foo'."""
379 return branch.replace('refs/heads/', '')
380
381
382class Changelist(object):
383 def __init__(self, branchref=None):
384 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000385 global settings
386 if not settings:
387 # Happens when git_cl.py is used as a utility library.
388 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000389 settings.GetDefaultServerUrl()
390 self.branchref = branchref
391 if self.branchref:
392 self.branch = ShortBranchName(self.branchref)
393 else:
394 self.branch = None
395 self.rietveld_server = None
396 self.upstream_branch = None
397 self.has_issue = False
398 self.issue = None
399 self.has_description = False
400 self.description = None
401 self.has_patchset = False
402 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000403 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000404 self.cc = None
405 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000406 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000407
408 def GetCCList(self):
409 """Return the users cc'd on this CL.
410
411 Return is a string suitable for passing to gcl with the --cc flag.
412 """
413 if self.cc is None:
414 base_cc = settings .GetDefaultCCList()
415 more_cc = ','.join(self.watchers)
416 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
417 return self.cc
418
419 def SetWatchers(self, watchers):
420 """Set the list of email addresses that should be cc'd based on the changed
421 files in this CL.
422 """
423 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000424
425 def GetBranch(self):
426 """Returns the short branch name, e.g. 'master'."""
427 if not self.branch:
428 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
429 self.branch = ShortBranchName(self.branchref)
430 return self.branch
431
432 def GetBranchRef(self):
433 """Returns the full branch name, e.g. 'refs/heads/master'."""
434 self.GetBranch() # Poke the lazy loader.
435 return self.branchref
436
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000437 @staticmethod
438 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000439 """Returns a tuple containg remote and remote ref,
440 e.g. 'origin', 'refs/heads/master'
441 """
442 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000443 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
444 error_ok=True).strip()
445 if upstream_branch:
446 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
447 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000448 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
449 error_ok=True).strip()
450 if upstream_branch:
451 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000453 # Fall back on trying a git-svn upstream branch.
454 if settings.GetIsGitSvn():
455 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000457 # Else, try to guess the origin remote.
458 remote_branches = RunGit(['branch', '-r']).split()
459 if 'origin/master' in remote_branches:
460 # Fall back on origin/master if it exits.
461 remote = 'origin'
462 upstream_branch = 'refs/heads/master'
463 elif 'origin/trunk' in remote_branches:
464 # Fall back on origin/trunk if it exists. Generally a shared
465 # git-svn clone
466 remote = 'origin'
467 upstream_branch = 'refs/heads/trunk'
468 else:
469 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470Either pass complete "git diff"-style arguments, like
471 git cl upload origin/master
472or verify this branch is set up to track another (via the --track argument to
473"git checkout -b ...").""")
474
475 return remote, upstream_branch
476
477 def GetUpstreamBranch(self):
478 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000479 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000480 if remote is not '.':
481 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
482 self.upstream_branch = upstream_branch
483 return self.upstream_branch
484
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000485 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000486 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000487 remote, branch = None, self.GetBranch()
488 seen_branches = set()
489 while branch not in seen_branches:
490 seen_branches.add(branch)
491 remote, branch = self.FetchUpstreamTuple(branch)
492 branch = ShortBranchName(branch)
493 if remote != '.' or branch.startswith('refs/remotes'):
494 break
495 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000496 remotes = RunGit(['remote'], error_ok=True).split()
497 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000498 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000499 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000500 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000501 logging.warning('Could not determine which remote this change is '
502 'associated with, so defaulting to "%s". This may '
503 'not be what you want. You may prevent this message '
504 'by running "git svn info" as documented here: %s',
505 self._remote,
506 GIT_INSTRUCTIONS_URL)
507 else:
508 logging.warn('Could not determine which remote this change is '
509 'associated with. You may prevent this message by '
510 'running "git svn info" as documented here: %s',
511 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000512 branch = 'HEAD'
513 if branch.startswith('refs/remotes'):
514 self._remote = (remote, branch)
515 else:
516 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000517 return self._remote
518
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000519 def GitSanityChecks(self, upstream_git_obj):
520 """Checks git repo status and ensures diff is from local commits."""
521
522 # Verify the commit we're diffing against is in our current branch.
523 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
524 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
525 if upstream_sha != common_ancestor:
526 print >> sys.stderr, (
527 'ERROR: %s is not in the current branch. You may need to rebase '
528 'your tracking branch' % upstream_sha)
529 return False
530
531 # List the commits inside the diff, and verify they are all local.
532 commits_in_diff = RunGit(
533 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
534 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
535 remote_branch = remote_branch.strip()
536 if code != 0:
537 _, remote_branch = self.GetRemoteBranch()
538
539 commits_in_remote = RunGit(
540 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
541
542 common_commits = set(commits_in_diff) & set(commits_in_remote)
543 if common_commits:
544 print >> sys.stderr, (
545 'ERROR: Your diff contains %d commits already in %s.\n'
546 'Run "git log --oneline %s..HEAD" to get a list of commits in '
547 'the diff. If you are using a custom git flow, you can override'
548 ' the reference used for this check with "git config '
549 'gitcl.remotebranch <git-ref>".' % (
550 len(common_commits), remote_branch, upstream_git_obj))
551 return False
552 return True
553
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000554 def GetGitBaseUrlFromConfig(self):
555 """Return the configured base URL from branch.<branchname>.baseurl.
556
557 Returns None if it is not set.
558 """
559 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
560 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562 def GetRemoteUrl(self):
563 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
564
565 Returns None if there is no remote.
566 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000567 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000568 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
569
570 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000571 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000572 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
574 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000575 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000576 else:
577 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000578 self.has_issue = True
579 return self.issue
580
581 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000582 if not self.rietveld_server:
583 # If we're on a branch then get the server potentially associated
584 # with that branch.
585 if self.GetIssue():
586 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
587 ['config', self._RietveldServer()], error_ok=True).strip())
588 if not self.rietveld_server:
589 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590 return self.rietveld_server
591
592 def GetIssueURL(self):
593 """Get the URL for a particular issue."""
594 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
595
596 def GetDescription(self, pretty=False):
597 if not self.has_description:
598 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000599 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000600 try:
601 self.description = self.RpcServer().get_description(issue).strip()
602 except urllib2.HTTPError, e:
603 if e.code == 404:
604 DieWithError(
605 ('\nWhile fetching the description for issue %d, received a '
606 '404 (not found)\n'
607 'error. It is likely that you deleted this '
608 'issue on the server. If this is the\n'
609 'case, please run\n\n'
610 ' git cl issue 0\n\n'
611 'to clear the association with the deleted issue. Then run '
612 'this command again.') % issue)
613 else:
614 DieWithError(
615 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 self.has_description = True
617 if pretty:
618 wrapper = textwrap.TextWrapper()
619 wrapper.initial_indent = wrapper.subsequent_indent = ' '
620 return wrapper.fill(self.description)
621 return self.description
622
623 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000624 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 if not self.has_patchset:
626 patchset = RunGit(['config', self._PatchsetSetting()],
627 error_ok=True).strip()
628 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000629 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 else:
631 self.patchset = None
632 self.has_patchset = True
633 return self.patchset
634
635 def SetPatchset(self, patchset):
636 """Set this branch's patchset. If patchset=0, clears the patchset."""
637 if patchset:
638 RunGit(['config', self._PatchsetSetting(), str(patchset)])
639 else:
640 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000641 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000642 self.has_patchset = False
643
binji@chromium.org0281f522012-09-14 13:37:59 +0000644 def GetMostRecentPatchset(self, issue):
645 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000646 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000647
648 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000649 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000650 '/download/issue%s_%s.diff' % (issue, patchset))
651
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000652 def GetApprovingReviewers(self, issue):
653 return get_approving_reviewers(
654 self.RpcServer().get_issue_properties(int(issue), True))
655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 def SetIssue(self, issue):
657 """Set this branch's issue. If issue=0, clears the issue."""
658 if issue:
659 RunGit(['config', self._IssueSetting(), str(issue)])
660 if self.rietveld_server:
661 RunGit(['config', self._RietveldServer(), self.rietveld_server])
662 else:
663 RunGit(['config', '--unset', self._IssueSetting()])
664 self.SetPatchset(0)
665 self.has_issue = False
666
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000667 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000668 if not self.GitSanityChecks(upstream_branch):
669 DieWithError('\nGit sanity check failure')
670
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000671 root = RunCommand(['git', '--no-pager', 'rev-parse', '--show-cdup']).strip()
672 if not root:
673 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000674 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000675
676 # We use the sha1 of HEAD as a name of this change.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000677 name = RunCommand(['git', '--no-pager', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000678 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000679 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000680 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000681 except subprocess2.CalledProcessError:
682 DieWithError(
683 ('\nFailed to diff against upstream branch %s!\n\n'
684 'This branch probably doesn\'t exist anymore. To reset the\n'
685 'tracking branch, please run\n'
686 ' git branch --set-upstream %s trunk\n'
687 'replacing trunk with origin/master or the relevant branch') %
688 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000689
maruel@chromium.org52424302012-08-29 15:14:30 +0000690 issue = self.GetIssue()
691 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000692 if issue:
693 description = self.GetDescription()
694 else:
695 # If the change was never uploaded, use the log messages of all commits
696 # up to the branch point, as git cl upload will prefill the description
697 # with these log messages.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000698 description = RunCommand(['git', '--no-pager',
699 'log', '--pretty=format:%s%n%n%b',
maruel@chromium.org373af802012-05-25 21:07:33 +0000700 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000701
702 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000703 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000704 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000705 name,
706 description,
707 absroot,
708 files,
709 issue,
710 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000711 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000712
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000713 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000714 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000715
716 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000717 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000718 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000719 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000720 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000721 except presubmit_support.PresubmitFailure, e:
722 DieWithError(
723 ('%s\nMaybe your depot_tools is out of date?\n'
724 'If all fails, contact maruel@') % e)
725
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000726 def UpdateDescription(self, description):
727 self.description = description
728 return self.RpcServer().update_description(
729 self.GetIssue(), self.description)
730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000732 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000733 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000735 def SetFlag(self, flag, value):
736 """Patchset must match."""
737 if not self.GetPatchset():
738 DieWithError('The patchset needs to match. Send another patchset.')
739 try:
740 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000741 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000742 except urllib2.HTTPError, e:
743 if e.code == 404:
744 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
745 if e.code == 403:
746 DieWithError(
747 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
748 'match?') % (self.GetIssue(), self.GetPatchset()))
749 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000751 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 """Returns an upload.RpcServer() to access this review's rietveld instance.
753 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000754 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000755 self._rpc_server = rietveld.CachingRietveld(
756 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000757 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758
759 def _IssueSetting(self):
760 """Return the git setting that stores this change's issue."""
761 return 'branch.%s.rietveldissue' % self.GetBranch()
762
763 def _PatchsetSetting(self):
764 """Return the git setting that stores this change's most recent patchset."""
765 return 'branch.%s.rietveldpatchset' % self.GetBranch()
766
767 def _RietveldServer(self):
768 """Returns the git setting that stores this change's rietveld server."""
769 return 'branch.%s.rietveldserver' % self.GetBranch()
770
771
772def GetCodereviewSettingsInteractively():
773 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000774 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 server = settings.GetDefaultServerUrl(error_ok=True)
776 prompt = 'Rietveld server (host[:port])'
777 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000778 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779 if not server and not newserver:
780 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000781 if newserver:
782 newserver = gclient_utils.UpgradeToHttps(newserver)
783 if newserver != server:
784 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000786 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 prompt = caption
788 if initial:
789 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000790 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 if new_val == 'x':
792 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000793 elif new_val:
794 if is_url:
795 new_val = gclient_utils.UpgradeToHttps(new_val)
796 if new_val != initial:
797 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000799 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000801 'tree-status-url', False)
802 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803
804 # TODO: configure a default branch to diff against, rather than this
805 # svn-based hackery.
806
807
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000808class ChangeDescription(object):
809 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000810 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000811
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000812 def __init__(self, description):
813 self._description = (description or '').strip()
814
815 @property
816 def description(self):
817 return self._description
818
819 def update_reviewers(self, reviewers):
820 """Rewrites the R=/TBR= line(s) as a single line."""
821 assert isinstance(reviewers, list), reviewers
822 if not reviewers:
823 return
824 regexp = re.compile(self.R_LINE, re.MULTILINE)
825 matches = list(regexp.finditer(self._description))
826 is_tbr = any(m.group(1) == 'TBR' for m in matches)
827 if len(matches) > 1:
828 # Erase all except the first one.
829 for i in xrange(len(matches) - 1, 0, -1):
830 self._description = (
831 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000832 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000833
834 if is_tbr:
835 new_r_line = 'TBR=' + ', '.join(reviewers)
836 else:
837 new_r_line = 'R=' + ', '.join(reviewers)
838
839 if matches:
840 self._description = (
841 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000842 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000843 else:
844 self.append_footer(new_r_line)
845
846 def prompt(self):
847 """Asks the user to update the description."""
848 self._description = (
849 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000850 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000851 '# The first line will also be used as the subject of the review.\n'
852 ) + self._description
853
854 if '\nBUG=' not in self._description:
855 self.append_footer('BUG=')
856 content = gclient_utils.RunEditor(self._description, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000857 if not content:
858 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000859
860 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000861 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000862 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000863 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000864 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000865
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000866 def append_footer(self, line):
867 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
868 if self._description:
869 if '\n' not in self._description:
870 self._description += '\n'
871 else:
872 last_line = self._description.rsplit('\n', 1)[1]
873 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
874 not presubmit_support.Change.TAG_LINE_RE.match(line)):
875 self._description += '\n'
876 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000877
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000878 def get_reviewers(self):
879 """Retrieves the list of reviewers."""
880 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000881 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000883
884
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000885def get_approving_reviewers(props):
886 """Retrieves the reviewers that approved a CL from the issue properties with
887 messages.
888
889 Note that the list may contain reviewers that are not committer, thus are not
890 considered by the CQ.
891 """
892 return sorted(
893 set(
894 message['sender']
895 for message in props['messages']
896 if message['approval'] and message['sender'] in props['reviewers']
897 )
898 )
899
900
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000901def FindCodereviewSettingsFile(filename='codereview.settings'):
902 """Finds the given file starting in the cwd and going up.
903
904 Only looks up to the top of the repository unless an
905 'inherit-review-settings-ok' file exists in the root of the repository.
906 """
907 inherit_ok_file = 'inherit-review-settings-ok'
908 cwd = os.getcwd()
909 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
910 if os.path.isfile(os.path.join(root, inherit_ok_file)):
911 root = '/'
912 while True:
913 if filename in os.listdir(cwd):
914 if os.path.isfile(os.path.join(cwd, filename)):
915 return open(os.path.join(cwd, filename))
916 if cwd == root:
917 break
918 cwd = os.path.dirname(cwd)
919
920
921def LoadCodereviewSettingsFromFile(fileobj):
922 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000923 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 def SetProperty(name, setting, unset_error_ok=False):
926 fullname = 'rietveld.' + name
927 if setting in keyvals:
928 RunGit(['config', fullname, keyvals[setting]])
929 else:
930 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
931
932 SetProperty('server', 'CODE_REVIEW_SERVER')
933 # Only server setting is required. Other settings can be absent.
934 # In that case, we ignore errors raised during option deletion attempt.
935 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
936 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
937 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
938
ukai@chromium.orge8077812012-02-03 03:41:46 +0000939 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
940 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
941 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
944 #should be of the form
945 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
946 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
947 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
948 keyvals['ORIGIN_URL_CONFIG']])
949
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000951def urlretrieve(source, destination):
952 """urllib is broken for SSL connections via a proxy therefore we
953 can't use urllib.urlretrieve()."""
954 with open(destination, 'w') as f:
955 f.write(urllib2.urlopen(source).read())
956
957
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000958def DownloadHooks(force):
959 """downloads hooks
960
961 Args:
962 force: True to update hooks. False to install hooks if not present.
963 """
964 if not settings.GetIsGerrit():
965 return
966 server_url = settings.GetDefaultServerUrl()
967 src = '%s/tools/hooks/commit-msg' % server_url
968 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
969 if not os.access(dst, os.X_OK):
970 if os.path.exists(dst):
971 if not force:
972 return
973 os.remove(dst)
974 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000975 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000976 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
977 except Exception:
978 if os.path.exists(dst):
979 os.remove(dst)
980 DieWithError('\nFailed to download hooks from %s' % src)
981
982
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983@usage('[repo root containing codereview.settings]')
984def CMDconfig(parser, args):
985 """edit configuration for this tree"""
986
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000987 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000988 if len(args) == 0:
989 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000990 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991 return 0
992
993 url = args[0]
994 if not url.endswith('codereview.settings'):
995 url = os.path.join(url, 'codereview.settings')
996
997 # Load code review settings and download hooks (if available).
998 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000999 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 return 0
1001
1002
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001003def CMDbaseurl(parser, args):
1004 """get or set base-url for this branch"""
1005 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1006 branch = ShortBranchName(branchref)
1007 _, args = parser.parse_args(args)
1008 if not args:
1009 print("Current base-url:")
1010 return RunGit(['config', 'branch.%s.base-url' % branch],
1011 error_ok=False).strip()
1012 else:
1013 print("Setting base-url to %s" % args[0])
1014 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1015 error_ok=False).strip()
1016
1017
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018def CMDstatus(parser, args):
1019 """show status of changelists"""
1020 parser.add_option('--field',
1021 help='print only specific field (desc|id|patch|url)')
1022 (options, args) = parser.parse_args(args)
1023
1024 # TODO: maybe make show_branches a flag if necessary.
1025 show_branches = not options.field
1026
1027 if show_branches:
1028 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1029 if branches:
1030 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001031 changes = (Changelist(branchref=b) for b in branches.splitlines())
1032 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1033 alignment = max(5, max(len(b) for b in branches))
1034 for branch in sorted(branches):
1035 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036
1037 cl = Changelist()
1038 if options.field:
1039 if options.field.startswith('desc'):
1040 print cl.GetDescription()
1041 elif options.field == 'id':
1042 issueid = cl.GetIssue()
1043 if issueid:
1044 print issueid
1045 elif options.field == 'patch':
1046 patchset = cl.GetPatchset()
1047 if patchset:
1048 print patchset
1049 elif options.field == 'url':
1050 url = cl.GetIssueURL()
1051 if url:
1052 print url
1053 else:
1054 print
1055 print 'Current branch:',
1056 if not cl.GetIssue():
1057 print 'no issue assigned.'
1058 return 0
1059 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001060 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 print 'Issue description:'
1062 print cl.GetDescription(pretty=True)
1063 return 0
1064
1065
1066@usage('[issue_number]')
1067def CMDissue(parser, args):
1068 """Set or display the current code review issue number.
1069
1070 Pass issue number 0 to clear the current issue.
1071"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001072 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001073
1074 cl = Changelist()
1075 if len(args) > 0:
1076 try:
1077 issue = int(args[0])
1078 except ValueError:
1079 DieWithError('Pass a number to set the issue or none to list it.\n'
1080 'Maybe you want to run git cl status?')
1081 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001082 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 return 0
1084
1085
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001086def CMDcomments(parser, args):
1087 """show review comments of the current changelist"""
1088 (_, args) = parser.parse_args(args)
1089 if args:
1090 parser.error('Unsupported argument: %s' % args)
1091
1092 cl = Changelist()
1093 if cl.GetIssue():
1094 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1095 for message in sorted(data['messages'], key=lambda x: x['date']):
1096 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1097 if message['text'].strip():
1098 print '\n'.join(' ' + l for l in message['text'].splitlines())
1099 return 0
1100
1101
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102def CreateDescriptionFromLog(args):
1103 """Pulls out the commit log to use as a base for the CL description."""
1104 log_args = []
1105 if len(args) == 1 and not args[0].endswith('.'):
1106 log_args = [args[0] + '..']
1107 elif len(args) == 1 and args[0].endswith('...'):
1108 log_args = [args[0][:-1]]
1109 elif len(args) == 2:
1110 log_args = [args[0] + '..' + args[1]]
1111 else:
1112 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001113 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114
1115
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116def CMDpresubmit(parser, args):
1117 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001118 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001120 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001121 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 (options, args) = parser.parse_args(args)
1123
ukai@chromium.org259e4682012-10-25 07:36:33 +00001124 if not options.force and is_dirty_git_tree('presubmit'):
1125 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 return 1
1127
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001128 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 if args:
1130 base_branch = args[0]
1131 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001132 # Default to diffing against the common ancestor of the upstream branch.
1133 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001135 cl.RunHook(
1136 committing=not options.upload,
1137 may_prompt=False,
1138 verbose=options.verbose,
1139 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001140 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141
1142
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001143def AddChangeIdToCommitMessage(options, args):
1144 """Re-commits using the current message, assumes the commit hook is in
1145 place.
1146 """
1147 log_desc = options.message or CreateDescriptionFromLog(args)
1148 git_command = ['commit', '--amend', '-m', log_desc]
1149 RunGit(git_command)
1150 new_log_desc = CreateDescriptionFromLog(args)
1151 if CHANGE_ID in new_log_desc:
1152 print 'git-cl: Added Change-Id to commit message.'
1153 else:
1154 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1155
1156
ukai@chromium.orge8077812012-02-03 03:41:46 +00001157def GerritUpload(options, args, cl):
1158 """upload the current branch to gerrit."""
1159 # We assume the remote called "origin" is the one we want.
1160 # It is probably not worthwhile to support different workflows.
1161 remote = 'origin'
1162 branch = 'master'
1163 if options.target_branch:
1164 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001166 change_desc = ChangeDescription(
1167 options.message or CreateDescriptionFromLog(args))
1168 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001169 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001171 if CHANGE_ID not in change_desc.description:
1172 AddChangeIdToCommitMessage(options, args)
1173 if options.reviewers:
1174 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175
ukai@chromium.orge8077812012-02-03 03:41:46 +00001176 receive_options = []
1177 cc = cl.GetCCList().split(',')
1178 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001179 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001180 cc = filter(None, cc)
1181 if cc:
1182 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001183 if change_desc.get_reviewers():
1184 receive_options.extend(
1185 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
ukai@chromium.orge8077812012-02-03 03:41:46 +00001187 git_command = ['push']
1188 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001189 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001190 ' '.join(receive_options))
1191 git_command += [remote, 'HEAD:refs/for/' + branch]
1192 RunGit(git_command)
1193 # TODO(ukai): parse Change-Id: and set issue number?
1194 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001195
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196
ukai@chromium.orge8077812012-02-03 03:41:46 +00001197def RietveldUpload(options, args, cl):
1198 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1200 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 if options.emulate_svn_auto_props:
1202 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203
1204 change_desc = None
1205
1206 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001207 if options.title:
1208 upload_args.extend(['--title', options.title])
1209 elif options.message:
1210 # TODO(rogerta): for now, the -m option will also set the --title option
1211 # for upload.py. Soon this will be changed to set the --message option.
1212 # Will wait until people are used to typing -t instead of -m.
1213 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001214 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 print ("This branch is associated with issue %s. "
1216 "Adding patch to that issue." % cl.GetIssue())
1217 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001218 if options.title:
1219 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001220 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001221 change_desc = ChangeDescription(message)
1222 if options.reviewers:
1223 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001224 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001225 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001226
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001227 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 print "Description is empty; aborting."
1229 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001230
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001231 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001232 if change_desc.get_reviewers():
1233 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001234 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001235 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001236 DieWithError("Must specify reviewers to send email.")
1237 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001238 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001239 if cc:
1240 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001242 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001243 if not options.find_copies:
1244 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001245
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 # Include the upstream repo's URL in the change -- this is useful for
1247 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001248 remote_url = cl.GetGitBaseUrlFromConfig()
1249 if not remote_url:
1250 if settings.GetIsGitSvn():
1251 # URL is dependent on the current directory.
1252 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1253 if data:
1254 keys = dict(line.split(': ', 1) for line in data.splitlines()
1255 if ': ' in line)
1256 remote_url = keys.get('URL', None)
1257 else:
1258 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1259 remote_url = (cl.GetRemoteUrl() + '@'
1260 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 if remote_url:
1262 upload_args.extend(['--base_url', remote_url])
1263
1264 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001265 upload_args = ['upload'] + upload_args + args
1266 logging.info('upload.RealMain(%s)', upload_args)
1267 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001268 except KeyboardInterrupt:
1269 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 except:
1271 # If we got an exception after the user typed a description for their
1272 # change, back up the description before re-raising.
1273 if change_desc:
1274 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1275 print '\nGot exception while uploading -- saving description to %s\n' \
1276 % backup_path
1277 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001278 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 backup_file.close()
1280 raise
1281
1282 if not cl.GetIssue():
1283 cl.SetIssue(issue)
1284 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001285
1286 if options.use_commit_queue:
1287 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 return 0
1289
1290
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001291def cleanup_list(l):
1292 """Fixes a list so that comma separated items are put as individual items.
1293
1294 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1295 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1296 """
1297 items = sum((i.split(',') for i in l), [])
1298 stripped_items = (i.strip() for i in items)
1299 return sorted(filter(None, stripped_items))
1300
1301
ukai@chromium.orge8077812012-02-03 03:41:46 +00001302@usage('[args to "git diff"]')
1303def CMDupload(parser, args):
1304 """upload the current changelist to codereview"""
1305 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1306 help='bypass upload presubmit hook')
1307 parser.add_option('-f', action='store_true', dest='force',
1308 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001309 parser.add_option('-m', dest='message', help='message for patchset')
1310 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001311 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001312 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001313 help='reviewer email addresses')
1314 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001315 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001316 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001317 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001318 help='send email to reviewer immediately')
1319 parser.add_option("--emulate_svn_auto_props", action="store_true",
1320 dest="emulate_svn_auto_props",
1321 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001322 parser.add_option('-c', '--use-commit-queue', action='store_true',
1323 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001324 parser.add_option('--target_branch',
1325 help='When uploading to gerrit, remote branch to '
1326 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001327 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001328 (options, args) = parser.parse_args(args)
1329
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001330 if options.target_branch and not settings.GetIsGerrit():
1331 parser.error('Use --target_branch for non gerrit repository.')
1332
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001333 # Print warning if the user used the -m/--message argument. This will soon
1334 # change to -t/--title.
1335 if options.message:
1336 print >> sys.stderr, (
1337 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1338 'In the near future, -m or --message will send a message instead.\n'
1339 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001340
ukai@chromium.org259e4682012-10-25 07:36:33 +00001341 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001342 return 1
1343
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001344 options.reviewers = cleanup_list(options.reviewers)
1345 options.cc = cleanup_list(options.cc)
1346
ukai@chromium.orge8077812012-02-03 03:41:46 +00001347 cl = Changelist()
1348 if args:
1349 # TODO(ukai): is it ok for gerrit case?
1350 base_branch = args[0]
1351 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001352 # Default to diffing against common ancestor of upstream branch
1353 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001354 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001355
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001356 # Apply watchlists on upload.
1357 change = cl.GetChange(base_branch, None)
1358 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1359 files = [f.LocalPath() for f in change.AffectedFiles()]
1360 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1361
ukai@chromium.orge8077812012-02-03 03:41:46 +00001362 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001363 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001364 may_prompt=not options.force,
1365 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001366 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001367 if not hook_results.should_continue():
1368 return 1
1369 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001370 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001371
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001372 if cl.GetIssue():
1373 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1374 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001375 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001376 print ('The last upload made from this repository was patchset #%d but '
1377 'the most recent patchset on the server is #%d.'
1378 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001379 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1380 'from another machine or branch the patch you\'re uploading now '
1381 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001382 ask_for_data('About to upload; enter to confirm.')
1383
iannucci@chromium.org79540052012-10-19 23:15:26 +00001384 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385 if settings.GetIsGerrit():
1386 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001387 ret = RietveldUpload(options, args, cl)
1388 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001389 git_set_branch_value('last-upload-hash',
1390 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001391
1392 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001393
1394
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001395def IsSubmoduleMergeCommit(ref):
1396 # When submodules are added to the repo, we expect there to be a single
1397 # non-git-svn merge commit at remote HEAD with a signature comment.
1398 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001399 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001400 return RunGit(cmd) != ''
1401
1402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403def SendUpstream(parser, args, cmd):
1404 """Common code for CmdPush and CmdDCommit
1405
1406 Squashed commit into a single.
1407 Updates changelog with metadata (e.g. pointer to review).
1408 Pushes/dcommits the code upstream.
1409 Updates review and closes.
1410 """
1411 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1412 help='bypass upload presubmit hook')
1413 parser.add_option('-m', dest='message',
1414 help="override review description")
1415 parser.add_option('-f', action='store_true', dest='force',
1416 help="force yes to questions (don't prompt)")
1417 parser.add_option('-c', dest='contributor',
1418 help="external contributor for patch (appended to " +
1419 "description and used as author for git). Should be " +
1420 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001421 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 (options, args) = parser.parse_args(args)
1423 cl = Changelist()
1424
1425 if not args or cmd == 'push':
1426 # Default to merging against our best guess of the upstream branch.
1427 args = [cl.GetUpstreamBranch()]
1428
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001429 if options.contributor:
1430 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1431 print "Please provide contibutor as 'First Last <email@example.com>'"
1432 return 1
1433
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001435 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436
ukai@chromium.org259e4682012-10-25 07:36:33 +00001437 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 return 1
1439
1440 # This rev-list syntax means "show all commits not in my branch that
1441 # are in base_branch".
1442 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1443 base_branch]).splitlines()
1444 if upstream_commits:
1445 print ('Base branch "%s" has %d commits '
1446 'not in this branch.' % (base_branch, len(upstream_commits)))
1447 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1448 return 1
1449
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001450 # This is the revision `svn dcommit` will commit on top of.
1451 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1452 '--pretty=format:%H'])
1453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001455 # If the base_head is a submodule merge commit, the first parent of the
1456 # base_head should be a git-svn commit, which is what we're interested in.
1457 base_svn_head = base_branch
1458 if base_has_submodules:
1459 base_svn_head += '^1'
1460
1461 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 if extra_commits:
1463 print ('This branch has %d additional commits not upstreamed yet.'
1464 % len(extra_commits.splitlines()))
1465 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1466 'before attempting to %s.' % (base_branch, cmd))
1467 return 1
1468
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001469 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001470 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001471 author = None
1472 if options.contributor:
1473 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001474 hook_results = cl.RunHook(
1475 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001476 may_prompt=not options.force,
1477 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001478 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001479 if not hook_results.should_continue():
1480 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481
1482 if cmd == 'dcommit':
1483 # Check the tree status if the tree status URL is set.
1484 status = GetTreeStatus()
1485 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001486 print('The tree is closed. Please wait for it to reopen. Use '
1487 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 return 1
1489 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001490 print('Unable to determine tree status. Please verify manually and '
1491 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001492 else:
1493 breakpad.SendStack(
1494 'GitClHooksBypassedCommit',
1495 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001496 (cl.GetRietveldServer(), cl.GetIssue()),
1497 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001499 change_desc = ChangeDescription(options.message)
1500 if not change_desc.description and cl.GetIssue():
1501 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001503 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001504 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001505 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001506 else:
1507 print 'No description set.'
1508 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1509 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001511 # Keep a separate copy for the commit message, because the commit message
1512 # contains the link to the Rietveld issue, while the Rietveld message contains
1513 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001514 # Keep a separate copy for the commit message.
1515 if cl.GetIssue():
1516 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1517
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001518 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001519 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001520 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001522 commit_desc.append_footer('Patch from %s.' % options.contributor)
1523
1524 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001525
1526 branches = [base_branch, cl.GetBranchRef()]
1527 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001528 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001529 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001531 # We want to squash all this branch's commits into one commit with the proper
1532 # description. We do this by doing a "reset --soft" to the base branch (which
1533 # keeps the working copy the same), then dcommitting that. If origin/master
1534 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1535 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001537 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1538 # Delete the branches if they exist.
1539 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1540 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1541 result = RunGitWithCode(showref_cmd)
1542 if result[0] == 0:
1543 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544
1545 # We might be in a directory that's present in this branch but not in the
1546 # trunk. Move up to the top of the tree so that git commands that expect a
1547 # valid CWD won't fail after we check out the merge branch.
1548 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1549 if rel_base_path:
1550 os.chdir(rel_base_path)
1551
1552 # Stuff our change into the merge branch.
1553 # We wrap in a try...finally block so if anything goes wrong,
1554 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001555 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001557 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1558 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001560 RunGit(
1561 [
1562 'commit', '--author', options.contributor,
1563 '-m', commit_desc.description,
1564 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001566 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001567 if base_has_submodules:
1568 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1569 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1570 RunGit(['checkout', CHERRY_PICK_BRANCH])
1571 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572 if cmd == 'push':
1573 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001574 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575 retcode, output = RunGitWithCode(
1576 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1577 logging.debug(output)
1578 else:
1579 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001580 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001581 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001582 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583 finally:
1584 # And then swap back to the original branch and clean up.
1585 RunGit(['checkout', '-q', cl.GetBranch()])
1586 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001587 if base_has_submodules:
1588 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589
1590 if cl.GetIssue():
1591 if cmd == 'dcommit' and 'Committed r' in output:
1592 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1593 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001594 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1595 for l in output.splitlines(False))
1596 match = filter(None, match)
1597 if len(match) != 1:
1598 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1599 output)
1600 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001601 else:
1602 return 1
1603 viewvc_url = settings.GetViewVCUrl()
1604 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001605 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001606 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001607 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001608 print ('Closing issue '
1609 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001610 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001612 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001613 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001614 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001615 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1616 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001617 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001618
1619 if retcode == 0:
1620 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1621 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001622 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001623
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624 return 0
1625
1626
1627@usage('[upstream branch to apply against]')
1628def CMDdcommit(parser, args):
1629 """commit the current changelist via git-svn"""
1630 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001631 message = """This doesn't appear to be an SVN repository.
1632If your project has a git mirror with an upstream SVN master, you probably need
1633to run 'git svn init', see your project's git mirror documentation.
1634If your project has a true writeable upstream repository, you probably want
1635to run 'git cl push' instead.
1636Choose wisely, if you get this wrong, your commit might appear to succeed but
1637will instead be silently ignored."""
1638 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001639 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 return SendUpstream(parser, args, 'dcommit')
1641
1642
1643@usage('[upstream branch to apply against]')
1644def CMDpush(parser, args):
1645 """commit the current changelist via git"""
1646 if settings.GetIsGitSvn():
1647 print('This appears to be an SVN repository.')
1648 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001649 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650 return SendUpstream(parser, args, 'push')
1651
1652
1653@usage('<patch url or issue id>')
1654def CMDpatch(parser, args):
1655 """patch in a code review"""
1656 parser.add_option('-b', dest='newbranch',
1657 help='create a new branch off trunk for the patch')
1658 parser.add_option('-f', action='store_true', dest='force',
1659 help='with -b, clobber any existing branch')
1660 parser.add_option('--reject', action='store_true', dest='reject',
1661 help='allow failed patches and spew .rej files')
1662 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1663 help="don't commit after patch applies")
1664 (options, args) = parser.parse_args(args)
1665 if len(args) != 1:
1666 parser.print_help()
1667 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001668 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001669
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001670 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001671 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001672
maruel@chromium.org52424302012-08-29 15:14:30 +00001673 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001675 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001676 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001677 patchset = cl.GetMostRecentPatchset(issue)
1678 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001679 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001680 # Assume it's a URL to the patch. Default to https.
1681 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001682 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001683 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001684 DieWithError('Must pass an issue ID or full URL for '
1685 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001686 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001687 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001688 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689
1690 if options.newbranch:
1691 if options.force:
1692 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001693 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001694 RunGit(['checkout', '-b', options.newbranch,
1695 Changelist().GetUpstreamBranch()])
1696
1697 # Switch up to the top-level directory, if necessary, in preparation for
1698 # applying the patch.
1699 top = RunGit(['rev-parse', '--show-cdup']).strip()
1700 if top:
1701 os.chdir(top)
1702
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001703 # Git patches have a/ at the beginning of source paths. We strip that out
1704 # with a sed script rather than the -p flag to patch so we can feed either
1705 # Git or svn-style patches into the same apply command.
1706 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001707 try:
1708 patch_data = subprocess2.check_output(
1709 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1710 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001711 DieWithError('Git patch mungling failed.')
1712 logging.info(patch_data)
1713 # We use "git apply" to apply the patch instead of "patch" so that we can
1714 # pick up file adds.
1715 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001716 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717 if options.reject:
1718 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001719 try:
1720 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1721 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 DieWithError('Failed to apply the patch')
1723
1724 # If we had an issue, commit the current state and register the issue.
1725 if not options.nocommit:
1726 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1727 cl = Changelist()
1728 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001729 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001730 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731 else:
1732 print "Patch applied to index."
1733 return 0
1734
1735
1736def CMDrebase(parser, args):
1737 """rebase current branch on top of svn repo"""
1738 # Provide a wrapper for git svn rebase to help avoid accidental
1739 # git svn dcommit.
1740 # It's the only command that doesn't use parser at all since we just defer
1741 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001742 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743
1744
1745def GetTreeStatus():
1746 """Fetches the tree status and returns either 'open', 'closed',
1747 'unknown' or 'unset'."""
1748 url = settings.GetTreeStatusUrl(error_ok=True)
1749 if url:
1750 status = urllib2.urlopen(url).read().lower()
1751 if status.find('closed') != -1 or status == '0':
1752 return 'closed'
1753 elif status.find('open') != -1 or status == '1':
1754 return 'open'
1755 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 return 'unset'
1757
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759def GetTreeStatusReason():
1760 """Fetches the tree status from a json url and returns the message
1761 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001762 url = settings.GetTreeStatusUrl()
1763 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764 connection = urllib2.urlopen(json_url)
1765 status = json.loads(connection.read())
1766 connection.close()
1767 return status['message']
1768
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001769
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001770def CMDtree(parser, args):
1771 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001772 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773 status = GetTreeStatus()
1774 if 'unset' == status:
1775 print 'You must configure your tree status URL by running "git cl config".'
1776 return 2
1777
1778 print "The tree is %s" % status
1779 print
1780 print GetTreeStatusReason()
1781 if status != 'open':
1782 return 1
1783 return 0
1784
1785
maruel@chromium.org15192402012-09-06 12:38:29 +00001786def CMDtry(parser, args):
1787 """Triggers a try job through Rietveld."""
1788 group = optparse.OptionGroup(parser, "Try job options")
1789 group.add_option(
1790 "-b", "--bot", action="append",
1791 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1792 "times to specify multiple builders. ex: "
1793 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1794 "the try server waterfall for the builders name and the tests "
1795 "available. Can also be used to specify gtest_filter, e.g. "
1796 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1797 group.add_option(
1798 "-r", "--revision",
1799 help="Revision to use for the try job; default: the "
1800 "revision will be determined by the try server; see "
1801 "its waterfall for more info")
1802 group.add_option(
1803 "-c", "--clobber", action="store_true", default=False,
1804 help="Force a clobber before building; e.g. don't do an "
1805 "incremental build")
1806 group.add_option(
1807 "--project",
1808 help="Override which project to use. Projects are defined "
1809 "server-side to define what default bot set to use")
1810 group.add_option(
1811 "-t", "--testfilter", action="append", default=[],
1812 help=("Apply a testfilter to all the selected builders. Unless the "
1813 "builders configurations are similar, use multiple "
1814 "--bot <builder>:<test> arguments."))
1815 group.add_option(
1816 "-n", "--name", help="Try job name; default to current branch name")
1817 parser.add_option_group(group)
1818 options, args = parser.parse_args(args)
1819
1820 if args:
1821 parser.error('Unknown arguments: %s' % args)
1822
1823 cl = Changelist()
1824 if not cl.GetIssue():
1825 parser.error('Need to upload first')
1826
1827 if not options.name:
1828 options.name = cl.GetBranch()
1829
1830 # Process --bot and --testfilter.
1831 if not options.bot:
1832 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001833 change = cl.GetChange(
1834 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1835 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001836 options.bot = presubmit_support.DoGetTrySlaves(
1837 change,
1838 change.LocalPaths(),
1839 settings.GetRoot(),
1840 None,
1841 None,
1842 options.verbose,
1843 sys.stdout)
1844 if not options.bot:
1845 parser.error('No default try builder to try, use --bot')
1846
1847 builders_and_tests = {}
1848 for bot in options.bot:
1849 if ':' in bot:
1850 builder, tests = bot.split(':', 1)
1851 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1852 elif ',' in bot:
1853 parser.error('Specify one bot per --bot flag')
1854 else:
1855 builders_and_tests.setdefault(bot, []).append('defaulttests')
1856
1857 if options.testfilter:
1858 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1859 builders_and_tests = dict(
1860 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1861 if t != ['compile'])
1862
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001863 if any('triggered' in b for b in builders_and_tests):
1864 print >> sys.stderr, (
1865 'ERROR You are trying to send a job to a triggered bot. This type of'
1866 ' bot requires an\ninitial job from a parent (usually a builder). '
1867 'Instead send your job to the parent.\n'
1868 'Bot list: %s' % builders_and_tests)
1869 return 1
1870
maruel@chromium.org15192402012-09-06 12:38:29 +00001871 patchset = cl.GetPatchset()
1872 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001873 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001874
1875 cl.RpcServer().trigger_try_jobs(
1876 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1877 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001878 print('Tried jobs on:')
1879 length = max(len(builder) for builder in builders_and_tests)
1880 for builder in sorted(builders_and_tests):
1881 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001882 return 0
1883
1884
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001885@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001887 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001888 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001889 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001890 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001891 return 0
1892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001894 if args:
1895 # One arg means set upstream branch.
1896 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1897 cl = Changelist()
1898 print "Upstream branch set to " + cl.GetUpstreamBranch()
1899 else:
1900 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901 return 0
1902
1903
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001904def CMDset_commit(parser, args):
1905 """set the commit bit"""
1906 _, args = parser.parse_args(args)
1907 if args:
1908 parser.error('Unrecognized args: %s' % ' '.join(args))
1909 cl = Changelist()
1910 cl.SetFlag('commit', '1')
1911 return 0
1912
1913
groby@chromium.org411034a2013-02-26 15:12:01 +00001914def CMDset_close(parser, args):
1915 """close the issue"""
1916 _, args = parser.parse_args(args)
1917 if args:
1918 parser.error('Unrecognized args: %s' % ' '.join(args))
1919 cl = Changelist()
1920 # Ensure there actually is an issue to close.
1921 cl.GetDescription()
1922 cl.CloseIssue()
1923 return 0
1924
1925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926def Command(name):
1927 return getattr(sys.modules[__name__], 'CMD' + name, None)
1928
1929
1930def CMDhelp(parser, args):
1931 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001932 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 if len(args) == 1:
1934 return main(args + ['--help'])
1935 parser.print_help()
1936 return 0
1937
1938
1939def GenUsage(parser, command):
1940 """Modify an OptParse object with the function's documentation."""
1941 obj = Command(command)
1942 more = getattr(obj, 'usage_more', '')
1943 if command == 'help':
1944 command = '<command>'
1945 else:
1946 # OptParser.description prefer nicely non-formatted strings.
1947 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1948 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1949
1950
1951def main(argv):
1952 """Doesn't parse the arguments here, just find the right subcommand to
1953 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001954 if sys.hexversion < 0x02060000:
1955 print >> sys.stderr, (
1956 '\nYour python version %s is unsupported, please upgrade.\n' %
1957 sys.version.split(' ', 1)[0])
1958 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001959 # Reload settings.
1960 global settings
1961 settings = Settings()
1962
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 # Do it late so all commands are listed.
1964 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1965 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1966 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1967
1968 # Create the option parse and add --verbose support.
1969 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001970 parser.add_option(
1971 '-v', '--verbose', action='count', default=0,
1972 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 old_parser_args = parser.parse_args
1974 def Parse(args):
1975 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001976 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001978 elif options.verbose:
1979 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001980 else:
1981 logging.basicConfig(level=logging.WARNING)
1982 return options, args
1983 parser.parse_args = Parse
1984
1985 if argv:
1986 command = Command(argv[0])
1987 if command:
1988 # "fix" the usage and the description now that we know the subcommand.
1989 GenUsage(parser, argv[0])
1990 try:
1991 return command(parser, argv[1:])
1992 except urllib2.HTTPError, e:
1993 if e.code != 500:
1994 raise
1995 DieWithError(
1996 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1997 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1998
1999 # Not a known command. Default to help.
2000 GenUsage(parser, 'help')
2001 return CMDhelp(parser, argv)
2002
2003
2004if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002005 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 sys.exit(main(sys.argv[1:]))