blob: cd477b908dd4bcac5d4f92b2329b9892eae5c5f1 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000027from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000028from third_party import upload
29import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000030import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000031import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000033import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000035import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import watchlists
37
38
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000039DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000040POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000042GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000043CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000044
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045# Shortcut since it quickly becomes redundant.
46Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000047
maruel@chromium.orgddd59412011-11-30 14:20:38 +000048# Initialized in main()
49settings = None
50
51
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000053 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054 sys.exit(1)
55
56
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000059 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000060 except subprocess2.CalledProcessError as e:
61 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000064 'Command "%s" failed.\n%s' % (
65 ' '.join(args), error_message or e.stdout or ''))
66 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
68
69def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 """Returns stdout."""
bratell@opera.comf267b0e2013-05-02 09:11:43 +000071 return RunCommand(['git', '--no-pager'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
73
74def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000075 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000076 try:
bratell@opera.comf267b0e2013-05-02 09:11:43 +000077 out, code = subprocess2.communicate(['git', '--no-pager'] + args,
78 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000079 return code, out[0]
80 except ValueError:
81 # When the subprocess fails, it returns None. That triggers a ValueError
82 # when trying to unpack the return value into (out, code).
83 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084
85
86def usage(more):
87 def hook(fn):
88 fn.usage_more = more
89 return fn
90 return hook
91
92
maruel@chromium.org90541732011-04-01 17:54:18 +000093def ask_for_data(prompt):
94 try:
95 return raw_input(prompt)
96 except KeyboardInterrupt:
97 # Hide the exception.
98 sys.exit(1)
99
100
iannucci@chromium.org79540052012-10-19 23:15:26 +0000101def git_set_branch_value(key, value):
102 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000103 if not branch:
104 return
105
106 cmd = ['config']
107 if isinstance(value, int):
108 cmd.append('--int')
109 git_key = 'branch.%s.%s' % (branch, key)
110 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000111
112
113def git_get_branch_default(key, default):
114 branch = Changelist().GetBranch()
115 if branch:
116 git_key = 'branch.%s.%s' % (branch, key)
117 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
118 try:
119 return int(stdout.strip())
120 except ValueError:
121 pass
122 return default
123
124
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000125def add_git_similarity(parser):
126 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000127 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000128 help='Sets the percentage that a pair of files need to match in order to'
129 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000130 parser.add_option(
131 '--find-copies', action='store_true',
132 help='Allows git to look for copies.')
133 parser.add_option(
134 '--no-find-copies', action='store_false', dest='find_copies',
135 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000136
137 old_parser_args = parser.parse_args
138 def Parse(args):
139 options, args = old_parser_args(args)
140
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000141 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000143 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000144 print('Note: Saving similarity of %d%% in git config.'
145 % options.similarity)
146 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000147
iannucci@chromium.org79540052012-10-19 23:15:26 +0000148 options.similarity = max(0, min(options.similarity, 100))
149
150 if options.find_copies is None:
151 options.find_copies = bool(
152 git_get_branch_default('git-find-copies', True))
153 else:
154 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155
156 print('Using %d%% similarity for rename/copy detection. '
157 'Override with --similarity.' % options.similarity)
158
159 return options, args
160 parser.parse_args = Parse
161
162
ukai@chromium.org259e4682012-10-25 07:36:33 +0000163def is_dirty_git_tree(cmd):
164 # Make sure index is up-to-date before running diff-index.
165 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
166 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
167 if dirty:
168 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
169 print 'Uncommitted files: (git diff-index --name-status HEAD)'
170 print dirty[:4096]
171 if len(dirty) > 4096:
172 print '... (run "git diff-index --name-status HEAD" to see full output).'
173 return True
174 return False
175
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000176
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000177def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
178 """Return the corresponding git ref if |base_url| together with |glob_spec|
179 matches the full |url|.
180
181 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
182 """
183 fetch_suburl, as_ref = glob_spec.split(':')
184 if allow_wildcards:
185 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
186 if glob_match:
187 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
188 # "branches/{472,597,648}/src:refs/remotes/svn/*".
189 branch_re = re.escape(base_url)
190 if glob_match.group(1):
191 branch_re += '/' + re.escape(glob_match.group(1))
192 wildcard = glob_match.group(2)
193 if wildcard == '*':
194 branch_re += '([^/]*)'
195 else:
196 # Escape and replace surrounding braces with parentheses and commas
197 # with pipe symbols.
198 wildcard = re.escape(wildcard)
199 wildcard = re.sub('^\\\\{', '(', wildcard)
200 wildcard = re.sub('\\\\,', '|', wildcard)
201 wildcard = re.sub('\\\\}$', ')', wildcard)
202 branch_re += wildcard
203 if glob_match.group(3):
204 branch_re += re.escape(glob_match.group(3))
205 match = re.match(branch_re, url)
206 if match:
207 return re.sub('\*$', match.group(1), as_ref)
208
209 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
210 if fetch_suburl:
211 full_url = base_url + '/' + fetch_suburl
212 else:
213 full_url = base_url
214 if full_url == url:
215 return as_ref
216 return None
217
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000218
iannucci@chromium.org79540052012-10-19 23:15:26 +0000219def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000220 """Prints statistics about the change to the user."""
221 # --no-ext-diff is broken in some versions of Git, so try to work around
222 # this by overriding the environment (but there is still a problem if the
223 # git config key "diff.external" is used).
224 env = os.environ.copy()
225 if 'GIT_EXTERNAL_DIFF' in env:
226 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000227
228 if find_copies:
229 similarity_options = ['--find-copies-harder', '-l100000',
230 '-C%s' % similarity]
231 else:
232 similarity_options = ['-M%s' % similarity]
233
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000234 return subprocess2.call(
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000235 ['git', '--no-pager',
236 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000237 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000238
239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000240class Settings(object):
241 def __init__(self):
242 self.default_server = None
243 self.cc = None
244 self.root = None
245 self.is_git_svn = None
246 self.svn_branch = None
247 self.tree_status_url = None
248 self.viewvc_url = None
249 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000250 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000251 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000252
253 def LazyUpdateIfNeeded(self):
254 """Updates the settings from a codereview.settings file, if available."""
255 if not self.updated:
256 cr_settings_file = FindCodereviewSettingsFile()
257 if cr_settings_file:
258 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000259 self.updated = True
260 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000261 self.updated = True
262
263 def GetDefaultServerUrl(self, error_ok=False):
264 if not self.default_server:
265 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000266 self.default_server = gclient_utils.UpgradeToHttps(
267 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268 if error_ok:
269 return self.default_server
270 if not self.default_server:
271 error_message = ('Could not find settings file. You must configure '
272 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000273 self.default_server = gclient_utils.UpgradeToHttps(
274 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 return self.default_server
276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 def GetRoot(self):
278 if not self.root:
279 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
280 return self.root
281
282 def GetIsGitSvn(self):
283 """Return true if this repo looks like it's using git-svn."""
284 if self.is_git_svn is None:
285 # If you have any "svn-remote.*" config keys, we think you're using svn.
286 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000287 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288 return self.is_git_svn
289
290 def GetSVNBranch(self):
291 if self.svn_branch is None:
292 if not self.GetIsGitSvn():
293 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
294
295 # Try to figure out which remote branch we're based on.
296 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000297 # 1) iterate through our branch history and find the svn URL.
298 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299
300 # regexp matching the git-svn line that contains the URL.
301 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
302
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000303 # We don't want to go through all of history, so read a line from the
304 # pipe at a time.
305 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000306 cmd = ['git', '--no-pager', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000307 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000308 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000309 for line in proc.stdout:
310 match = git_svn_re.match(line)
311 if match:
312 url = match.group(1)
313 proc.stdout.close() # Cut pipe.
314 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000315
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000316 if url:
317 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
318 remotes = RunGit(['config', '--get-regexp',
319 r'^svn-remote\..*\.url']).splitlines()
320 for remote in remotes:
321 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000323 remote = match.group(1)
324 base_url = match.group(2)
325 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000326 ['config', 'svn-remote.%s.fetch' % remote],
327 error_ok=True).strip()
328 if fetch_spec:
329 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
330 if self.svn_branch:
331 break
332 branch_spec = RunGit(
333 ['config', 'svn-remote.%s.branches' % remote],
334 error_ok=True).strip()
335 if branch_spec:
336 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
337 if self.svn_branch:
338 break
339 tag_spec = RunGit(
340 ['config', 'svn-remote.%s.tags' % remote],
341 error_ok=True).strip()
342 if tag_spec:
343 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
344 if self.svn_branch:
345 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000346
347 if not self.svn_branch:
348 DieWithError('Can\'t guess svn branch -- try specifying it on the '
349 'command line')
350
351 return self.svn_branch
352
353 def GetTreeStatusUrl(self, error_ok=False):
354 if not self.tree_status_url:
355 error_message = ('You must configure your tree status URL by running '
356 '"git cl config".')
357 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
358 error_ok=error_ok,
359 error_message=error_message)
360 return self.tree_status_url
361
362 def GetViewVCUrl(self):
363 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000364 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365 return self.viewvc_url
366
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000367 def GetDefaultCCList(self):
368 return self._GetConfig('rietveld.cc', error_ok=True)
369
ukai@chromium.orge8077812012-02-03 03:41:46 +0000370 def GetIsGerrit(self):
371 """Return true if this repo is assosiated with gerrit code review system."""
372 if self.is_gerrit is None:
373 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
374 return self.is_gerrit
375
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000376 def GetGitEditor(self):
377 """Return the editor specified in the git config, or None if none is."""
378 if self.git_editor is None:
379 self.git_editor = self._GetConfig('core.editor', error_ok=True)
380 return self.git_editor or None
381
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000382 def _GetConfig(self, param, **kwargs):
383 self.LazyUpdateIfNeeded()
384 return RunGit(['config', param], **kwargs).strip()
385
386
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000387def ShortBranchName(branch):
388 """Convert a name like 'refs/heads/foo' to just 'foo'."""
389 return branch.replace('refs/heads/', '')
390
391
392class Changelist(object):
393 def __init__(self, branchref=None):
394 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000395 global settings
396 if not settings:
397 # Happens when git_cl.py is used as a utility library.
398 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000399 settings.GetDefaultServerUrl()
400 self.branchref = branchref
401 if self.branchref:
402 self.branch = ShortBranchName(self.branchref)
403 else:
404 self.branch = None
405 self.rietveld_server = None
406 self.upstream_branch = None
407 self.has_issue = False
408 self.issue = None
409 self.has_description = False
410 self.description = None
411 self.has_patchset = False
412 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000413 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000414 self.cc = None
415 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000416 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000417
418 def GetCCList(self):
419 """Return the users cc'd on this CL.
420
421 Return is a string suitable for passing to gcl with the --cc flag.
422 """
423 if self.cc is None:
424 base_cc = settings .GetDefaultCCList()
425 more_cc = ','.join(self.watchers)
426 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
427 return self.cc
428
429 def SetWatchers(self, watchers):
430 """Set the list of email addresses that should be cc'd based on the changed
431 files in this CL.
432 """
433 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000434
435 def GetBranch(self):
436 """Returns the short branch name, e.g. 'master'."""
437 if not self.branch:
438 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
439 self.branch = ShortBranchName(self.branchref)
440 return self.branch
441
442 def GetBranchRef(self):
443 """Returns the full branch name, e.g. 'refs/heads/master'."""
444 self.GetBranch() # Poke the lazy loader.
445 return self.branchref
446
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000447 @staticmethod
448 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 """Returns a tuple containg remote and remote ref,
450 e.g. 'origin', 'refs/heads/master'
451 """
452 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
454 error_ok=True).strip()
455 if upstream_branch:
456 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
457 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000458 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
459 error_ok=True).strip()
460 if upstream_branch:
461 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000462 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000463 # Fall back on trying a git-svn upstream branch.
464 if settings.GetIsGitSvn():
465 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000467 # Else, try to guess the origin remote.
468 remote_branches = RunGit(['branch', '-r']).split()
469 if 'origin/master' in remote_branches:
470 # Fall back on origin/master if it exits.
471 remote = 'origin'
472 upstream_branch = 'refs/heads/master'
473 elif 'origin/trunk' in remote_branches:
474 # Fall back on origin/trunk if it exists. Generally a shared
475 # git-svn clone
476 remote = 'origin'
477 upstream_branch = 'refs/heads/trunk'
478 else:
479 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000480Either pass complete "git diff"-style arguments, like
481 git cl upload origin/master
482or verify this branch is set up to track another (via the --track argument to
483"git checkout -b ...").""")
484
485 return remote, upstream_branch
486
487 def GetUpstreamBranch(self):
488 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000489 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000490 if remote is not '.':
491 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
492 self.upstream_branch = upstream_branch
493 return self.upstream_branch
494
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000495 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000496 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000497 remote, branch = None, self.GetBranch()
498 seen_branches = set()
499 while branch not in seen_branches:
500 seen_branches.add(branch)
501 remote, branch = self.FetchUpstreamTuple(branch)
502 branch = ShortBranchName(branch)
503 if remote != '.' or branch.startswith('refs/remotes'):
504 break
505 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000506 remotes = RunGit(['remote'], error_ok=True).split()
507 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000508 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000509 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000510 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000511 logging.warning('Could not determine which remote this change is '
512 'associated with, so defaulting to "%s". This may '
513 'not be what you want. You may prevent this message '
514 'by running "git svn info" as documented here: %s',
515 self._remote,
516 GIT_INSTRUCTIONS_URL)
517 else:
518 logging.warn('Could not determine which remote this change is '
519 'associated with. You may prevent this message by '
520 'running "git svn info" as documented here: %s',
521 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000522 branch = 'HEAD'
523 if branch.startswith('refs/remotes'):
524 self._remote = (remote, branch)
525 else:
526 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000527 return self._remote
528
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000529 def GitSanityChecks(self, upstream_git_obj):
530 """Checks git repo status and ensures diff is from local commits."""
531
532 # Verify the commit we're diffing against is in our current branch.
533 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
534 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
535 if upstream_sha != common_ancestor:
536 print >> sys.stderr, (
537 'ERROR: %s is not in the current branch. You may need to rebase '
538 'your tracking branch' % upstream_sha)
539 return False
540
541 # List the commits inside the diff, and verify they are all local.
542 commits_in_diff = RunGit(
543 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
544 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
545 remote_branch = remote_branch.strip()
546 if code != 0:
547 _, remote_branch = self.GetRemoteBranch()
548
549 commits_in_remote = RunGit(
550 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
551
552 common_commits = set(commits_in_diff) & set(commits_in_remote)
553 if common_commits:
554 print >> sys.stderr, (
555 'ERROR: Your diff contains %d commits already in %s.\n'
556 'Run "git log --oneline %s..HEAD" to get a list of commits in '
557 'the diff. If you are using a custom git flow, you can override'
558 ' the reference used for this check with "git config '
559 'gitcl.remotebranch <git-ref>".' % (
560 len(common_commits), remote_branch, upstream_git_obj))
561 return False
562 return True
563
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000564 def GetGitBaseUrlFromConfig(self):
565 """Return the configured base URL from branch.<branchname>.baseurl.
566
567 Returns None if it is not set.
568 """
569 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
570 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000571
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000572 def GetRemoteUrl(self):
573 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
574
575 Returns None if there is no remote.
576 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000577 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000578 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
579
580 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000581 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000582 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
584 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000585 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586 else:
587 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.has_issue = True
589 return self.issue
590
591 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000592 if not self.rietveld_server:
593 # If we're on a branch then get the server potentially associated
594 # with that branch.
595 if self.GetIssue():
596 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
597 ['config', self._RietveldServer()], error_ok=True).strip())
598 if not self.rietveld_server:
599 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 return self.rietveld_server
601
602 def GetIssueURL(self):
603 """Get the URL for a particular issue."""
604 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
605
606 def GetDescription(self, pretty=False):
607 if not self.has_description:
608 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000609 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000610 try:
611 self.description = self.RpcServer().get_description(issue).strip()
612 except urllib2.HTTPError, e:
613 if e.code == 404:
614 DieWithError(
615 ('\nWhile fetching the description for issue %d, received a '
616 '404 (not found)\n'
617 'error. It is likely that you deleted this '
618 'issue on the server. If this is the\n'
619 'case, please run\n\n'
620 ' git cl issue 0\n\n'
621 'to clear the association with the deleted issue. Then run '
622 'this command again.') % issue)
623 else:
624 DieWithError(
625 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 self.has_description = True
627 if pretty:
628 wrapper = textwrap.TextWrapper()
629 wrapper.initial_indent = wrapper.subsequent_indent = ' '
630 return wrapper.fill(self.description)
631 return self.description
632
633 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000634 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 if not self.has_patchset:
636 patchset = RunGit(['config', self._PatchsetSetting()],
637 error_ok=True).strip()
638 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000639 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640 else:
641 self.patchset = None
642 self.has_patchset = True
643 return self.patchset
644
645 def SetPatchset(self, patchset):
646 """Set this branch's patchset. If patchset=0, clears the patchset."""
647 if patchset:
648 RunGit(['config', self._PatchsetSetting(), str(patchset)])
649 else:
650 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000651 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 self.has_patchset = False
653
binji@chromium.org0281f522012-09-14 13:37:59 +0000654 def GetMostRecentPatchset(self, issue):
655 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000656 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000657
658 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000659 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000660 '/download/issue%s_%s.diff' % (issue, patchset))
661
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000662 def GetApprovingReviewers(self, issue):
663 return get_approving_reviewers(
664 self.RpcServer().get_issue_properties(int(issue), True))
665
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666 def SetIssue(self, issue):
667 """Set this branch's issue. If issue=0, clears the issue."""
668 if issue:
669 RunGit(['config', self._IssueSetting(), str(issue)])
670 if self.rietveld_server:
671 RunGit(['config', self._RietveldServer(), self.rietveld_server])
672 else:
673 RunGit(['config', '--unset', self._IssueSetting()])
674 self.SetPatchset(0)
675 self.has_issue = False
676
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000677 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000678 if not self.GitSanityChecks(upstream_branch):
679 DieWithError('\nGit sanity check failure')
680
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000681 root = RunCommand(['git', '--no-pager', 'rev-parse', '--show-cdup']).strip()
682 if not root:
683 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000684 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000685
686 # We use the sha1 of HEAD as a name of this change.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000687 name = RunCommand(['git', '--no-pager', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000688 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000689 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000690 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000691 except subprocess2.CalledProcessError:
692 DieWithError(
693 ('\nFailed to diff against upstream branch %s!\n\n'
694 'This branch probably doesn\'t exist anymore. To reset the\n'
695 'tracking branch, please run\n'
696 ' git branch --set-upstream %s trunk\n'
697 'replacing trunk with origin/master or the relevant branch') %
698 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000699
maruel@chromium.org52424302012-08-29 15:14:30 +0000700 issue = self.GetIssue()
701 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000702 if issue:
703 description = self.GetDescription()
704 else:
705 # If the change was never uploaded, use the log messages of all commits
706 # up to the branch point, as git cl upload will prefill the description
707 # with these log messages.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000708 description = RunCommand(['git', '--no-pager',
709 'log', '--pretty=format:%s%n%n%b',
maruel@chromium.org373af802012-05-25 21:07:33 +0000710 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000711
712 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000713 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000714 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000715 name,
716 description,
717 absroot,
718 files,
719 issue,
720 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000721 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000722
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000723 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000724 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000725
726 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000727 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000728 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000729 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000730 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000731 except presubmit_support.PresubmitFailure, e:
732 DieWithError(
733 ('%s\nMaybe your depot_tools is out of date?\n'
734 'If all fails, contact maruel@') % e)
735
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000736 def UpdateDescription(self, description):
737 self.description = description
738 return self.RpcServer().update_description(
739 self.GetIssue(), self.description)
740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000742 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000743 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000745 def SetFlag(self, flag, value):
746 """Patchset must match."""
747 if not self.GetPatchset():
748 DieWithError('The patchset needs to match. Send another patchset.')
749 try:
750 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000751 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000752 except urllib2.HTTPError, e:
753 if e.code == 404:
754 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
755 if e.code == 403:
756 DieWithError(
757 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
758 'match?') % (self.GetIssue(), self.GetPatchset()))
759 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000761 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 """Returns an upload.RpcServer() to access this review's rietveld instance.
763 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000764 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000765 self._rpc_server = rietveld.CachingRietveld(
766 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000767 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768
769 def _IssueSetting(self):
770 """Return the git setting that stores this change's issue."""
771 return 'branch.%s.rietveldissue' % self.GetBranch()
772
773 def _PatchsetSetting(self):
774 """Return the git setting that stores this change's most recent patchset."""
775 return 'branch.%s.rietveldpatchset' % self.GetBranch()
776
777 def _RietveldServer(self):
778 """Returns the git setting that stores this change's rietveld server."""
779 return 'branch.%s.rietveldserver' % self.GetBranch()
780
781
782def GetCodereviewSettingsInteractively():
783 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000784 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 server = settings.GetDefaultServerUrl(error_ok=True)
786 prompt = 'Rietveld server (host[:port])'
787 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000788 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 if not server and not newserver:
790 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000791 if newserver:
792 newserver = gclient_utils.UpgradeToHttps(newserver)
793 if newserver != server:
794 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000796 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 prompt = caption
798 if initial:
799 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000800 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 if new_val == 'x':
802 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000803 elif new_val:
804 if is_url:
805 new_val = gclient_utils.UpgradeToHttps(new_val)
806 if new_val != initial:
807 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000809 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000811 'tree-status-url', False)
812 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813
814 # TODO: configure a default branch to diff against, rather than this
815 # svn-based hackery.
816
817
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000818class ChangeDescription(object):
819 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000820 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000821
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000822 def __init__(self, description):
823 self._description = (description or '').strip()
824
825 @property
826 def description(self):
827 return self._description
828
829 def update_reviewers(self, reviewers):
830 """Rewrites the R=/TBR= line(s) as a single line."""
831 assert isinstance(reviewers, list), reviewers
832 if not reviewers:
833 return
834 regexp = re.compile(self.R_LINE, re.MULTILINE)
835 matches = list(regexp.finditer(self._description))
836 is_tbr = any(m.group(1) == 'TBR' for m in matches)
837 if len(matches) > 1:
838 # Erase all except the first one.
839 for i in xrange(len(matches) - 1, 0, -1):
840 self._description = (
841 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000842 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000843
844 if is_tbr:
845 new_r_line = 'TBR=' + ', '.join(reviewers)
846 else:
847 new_r_line = 'R=' + ', '.join(reviewers)
848
849 if matches:
850 self._description = (
851 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000852 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000853 else:
854 self.append_footer(new_r_line)
855
856 def prompt(self):
857 """Asks the user to update the description."""
858 self._description = (
859 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000860 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000861 '# The first line will also be used as the subject of the review.\n'
862 ) + self._description
863
864 if '\nBUG=' not in self._description:
865 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000866 content = gclient_utils.RunEditor(self._description, True,
867 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000868 if not content:
869 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000870
871 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000872 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000873 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000874 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000875 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000876
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000877 def append_footer(self, line):
878 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
879 if self._description:
880 if '\n' not in self._description:
881 self._description += '\n'
882 else:
883 last_line = self._description.rsplit('\n', 1)[1]
884 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
885 not presubmit_support.Change.TAG_LINE_RE.match(line)):
886 self._description += '\n'
887 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000888
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000889 def get_reviewers(self):
890 """Retrieves the list of reviewers."""
891 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000892 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000893 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000894
895
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000896def get_approving_reviewers(props):
897 """Retrieves the reviewers that approved a CL from the issue properties with
898 messages.
899
900 Note that the list may contain reviewers that are not committer, thus are not
901 considered by the CQ.
902 """
903 return sorted(
904 set(
905 message['sender']
906 for message in props['messages']
907 if message['approval'] and message['sender'] in props['reviewers']
908 )
909 )
910
911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912def FindCodereviewSettingsFile(filename='codereview.settings'):
913 """Finds the given file starting in the cwd and going up.
914
915 Only looks up to the top of the repository unless an
916 'inherit-review-settings-ok' file exists in the root of the repository.
917 """
918 inherit_ok_file = 'inherit-review-settings-ok'
919 cwd = os.getcwd()
920 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
921 if os.path.isfile(os.path.join(root, inherit_ok_file)):
922 root = '/'
923 while True:
924 if filename in os.listdir(cwd):
925 if os.path.isfile(os.path.join(cwd, filename)):
926 return open(os.path.join(cwd, filename))
927 if cwd == root:
928 break
929 cwd = os.path.dirname(cwd)
930
931
932def LoadCodereviewSettingsFromFile(fileobj):
933 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000934 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 def SetProperty(name, setting, unset_error_ok=False):
937 fullname = 'rietveld.' + name
938 if setting in keyvals:
939 RunGit(['config', fullname, keyvals[setting]])
940 else:
941 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
942
943 SetProperty('server', 'CODE_REVIEW_SERVER')
944 # Only server setting is required. Other settings can be absent.
945 # In that case, we ignore errors raised during option deletion attempt.
946 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
947 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
948 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
949
ukai@chromium.orge8077812012-02-03 03:41:46 +0000950 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
951 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
952 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000953
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
955 #should be of the form
956 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
957 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
958 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
959 keyvals['ORIGIN_URL_CONFIG']])
960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000962def urlretrieve(source, destination):
963 """urllib is broken for SSL connections via a proxy therefore we
964 can't use urllib.urlretrieve()."""
965 with open(destination, 'w') as f:
966 f.write(urllib2.urlopen(source).read())
967
968
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000969def DownloadHooks(force):
970 """downloads hooks
971
972 Args:
973 force: True to update hooks. False to install hooks if not present.
974 """
975 if not settings.GetIsGerrit():
976 return
977 server_url = settings.GetDefaultServerUrl()
978 src = '%s/tools/hooks/commit-msg' % server_url
979 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
980 if not os.access(dst, os.X_OK):
981 if os.path.exists(dst):
982 if not force:
983 return
984 os.remove(dst)
985 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000986 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000987 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
988 except Exception:
989 if os.path.exists(dst):
990 os.remove(dst)
991 DieWithError('\nFailed to download hooks from %s' % src)
992
993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000994@usage('[repo root containing codereview.settings]')
995def CMDconfig(parser, args):
996 """edit configuration for this tree"""
997
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000998 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 if len(args) == 0:
1000 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001001 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 return 0
1003
1004 url = args[0]
1005 if not url.endswith('codereview.settings'):
1006 url = os.path.join(url, 'codereview.settings')
1007
1008 # Load code review settings and download hooks (if available).
1009 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001010 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 return 0
1012
1013
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001014def CMDbaseurl(parser, args):
1015 """get or set base-url for this branch"""
1016 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1017 branch = ShortBranchName(branchref)
1018 _, args = parser.parse_args(args)
1019 if not args:
1020 print("Current base-url:")
1021 return RunGit(['config', 'branch.%s.base-url' % branch],
1022 error_ok=False).strip()
1023 else:
1024 print("Setting base-url to %s" % args[0])
1025 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1026 error_ok=False).strip()
1027
1028
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029def CMDstatus(parser, args):
1030 """show status of changelists"""
1031 parser.add_option('--field',
1032 help='print only specific field (desc|id|patch|url)')
1033 (options, args) = parser.parse_args(args)
1034
1035 # TODO: maybe make show_branches a flag if necessary.
1036 show_branches = not options.field
1037
1038 if show_branches:
1039 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1040 if branches:
1041 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001042 changes = (Changelist(branchref=b) for b in branches.splitlines())
1043 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1044 alignment = max(5, max(len(b) for b in branches))
1045 for branch in sorted(branches):
1046 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047
1048 cl = Changelist()
1049 if options.field:
1050 if options.field.startswith('desc'):
1051 print cl.GetDescription()
1052 elif options.field == 'id':
1053 issueid = cl.GetIssue()
1054 if issueid:
1055 print issueid
1056 elif options.field == 'patch':
1057 patchset = cl.GetPatchset()
1058 if patchset:
1059 print patchset
1060 elif options.field == 'url':
1061 url = cl.GetIssueURL()
1062 if url:
1063 print url
1064 else:
1065 print
1066 print 'Current branch:',
1067 if not cl.GetIssue():
1068 print 'no issue assigned.'
1069 return 0
1070 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001071 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 print 'Issue description:'
1073 print cl.GetDescription(pretty=True)
1074 return 0
1075
1076
1077@usage('[issue_number]')
1078def CMDissue(parser, args):
1079 """Set or display the current code review issue number.
1080
1081 Pass issue number 0 to clear the current issue.
1082"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001083 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084
1085 cl = Changelist()
1086 if len(args) > 0:
1087 try:
1088 issue = int(args[0])
1089 except ValueError:
1090 DieWithError('Pass a number to set the issue or none to list it.\n'
1091 'Maybe you want to run git cl status?')
1092 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001093 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 return 0
1095
1096
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001097def CMDcomments(parser, args):
1098 """show review comments of the current changelist"""
1099 (_, args) = parser.parse_args(args)
1100 if args:
1101 parser.error('Unsupported argument: %s' % args)
1102
1103 cl = Changelist()
1104 if cl.GetIssue():
1105 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1106 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001107 if message['disapproval']:
1108 color = Fore.RED
1109 elif message['approval']:
1110 color = Fore.GREEN
1111 elif message['sender'] == data['owner_email']:
1112 color = Fore.MAGENTA
1113 else:
1114 color = Fore.BLUE
1115 print '\n%s%s %s%s' % (
1116 color, message['date'].split('.', 1)[0], message['sender'],
1117 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001118 if message['text'].strip():
1119 print '\n'.join(' ' + l for l in message['text'].splitlines())
1120 return 0
1121
1122
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123def CreateDescriptionFromLog(args):
1124 """Pulls out the commit log to use as a base for the CL description."""
1125 log_args = []
1126 if len(args) == 1 and not args[0].endswith('.'):
1127 log_args = [args[0] + '..']
1128 elif len(args) == 1 and args[0].endswith('...'):
1129 log_args = [args[0][:-1]]
1130 elif len(args) == 2:
1131 log_args = [args[0] + '..' + args[1]]
1132 else:
1133 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001134 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135
1136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137def CMDpresubmit(parser, args):
1138 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001139 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001141 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001142 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 (options, args) = parser.parse_args(args)
1144
ukai@chromium.org259e4682012-10-25 07:36:33 +00001145 if not options.force and is_dirty_git_tree('presubmit'):
1146 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 return 1
1148
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001149 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 if args:
1151 base_branch = args[0]
1152 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001153 # Default to diffing against the common ancestor of the upstream branch.
1154 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001156 cl.RunHook(
1157 committing=not options.upload,
1158 may_prompt=False,
1159 verbose=options.verbose,
1160 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001161 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162
1163
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001164def AddChangeIdToCommitMessage(options, args):
1165 """Re-commits using the current message, assumes the commit hook is in
1166 place.
1167 """
1168 log_desc = options.message or CreateDescriptionFromLog(args)
1169 git_command = ['commit', '--amend', '-m', log_desc]
1170 RunGit(git_command)
1171 new_log_desc = CreateDescriptionFromLog(args)
1172 if CHANGE_ID in new_log_desc:
1173 print 'git-cl: Added Change-Id to commit message.'
1174 else:
1175 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1176
1177
ukai@chromium.orge8077812012-02-03 03:41:46 +00001178def GerritUpload(options, args, cl):
1179 """upload the current branch to gerrit."""
1180 # We assume the remote called "origin" is the one we want.
1181 # It is probably not worthwhile to support different workflows.
1182 remote = 'origin'
1183 branch = 'master'
1184 if options.target_branch:
1185 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001187 change_desc = ChangeDescription(
1188 options.message or CreateDescriptionFromLog(args))
1189 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001190 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001192 if CHANGE_ID not in change_desc.description:
1193 AddChangeIdToCommitMessage(options, args)
1194 if options.reviewers:
1195 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196
ukai@chromium.orge8077812012-02-03 03:41:46 +00001197 receive_options = []
1198 cc = cl.GetCCList().split(',')
1199 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001200 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001201 cc = filter(None, cc)
1202 if cc:
1203 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001204 if change_desc.get_reviewers():
1205 receive_options.extend(
1206 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207
ukai@chromium.orge8077812012-02-03 03:41:46 +00001208 git_command = ['push']
1209 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001210 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001211 ' '.join(receive_options))
1212 git_command += [remote, 'HEAD:refs/for/' + branch]
1213 RunGit(git_command)
1214 # TODO(ukai): parse Change-Id: and set issue number?
1215 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217
ukai@chromium.orge8077812012-02-03 03:41:46 +00001218def RietveldUpload(options, args, cl):
1219 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1221 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 if options.emulate_svn_auto_props:
1223 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224
1225 change_desc = None
1226
1227 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001228 if options.title:
1229 upload_args.extend(['--title', options.title])
1230 elif options.message:
1231 # TODO(rogerta): for now, the -m option will also set the --title option
1232 # for upload.py. Soon this will be changed to set the --message option.
1233 # Will wait until people are used to typing -t instead of -m.
1234 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001235 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 print ("This branch is associated with issue %s. "
1237 "Adding patch to that issue." % cl.GetIssue())
1238 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001239 if options.title:
1240 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001241 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001242 change_desc = ChangeDescription(message)
1243 if options.reviewers:
1244 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001245 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001246 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001247
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001248 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 print "Description is empty; aborting."
1250 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001251
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001252 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001253 if change_desc.get_reviewers():
1254 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001255 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001256 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001257 DieWithError("Must specify reviewers to send email.")
1258 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001259 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001260 if cc:
1261 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001263 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001264 if not options.find_copies:
1265 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 # Include the upstream repo's URL in the change -- this is useful for
1268 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001269 remote_url = cl.GetGitBaseUrlFromConfig()
1270 if not remote_url:
1271 if settings.GetIsGitSvn():
1272 # URL is dependent on the current directory.
1273 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1274 if data:
1275 keys = dict(line.split(': ', 1) for line in data.splitlines()
1276 if ': ' in line)
1277 remote_url = keys.get('URL', None)
1278 else:
1279 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1280 remote_url = (cl.GetRemoteUrl() + '@'
1281 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 if remote_url:
1283 upload_args.extend(['--base_url', remote_url])
1284
1285 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001286 upload_args = ['upload'] + upload_args + args
1287 logging.info('upload.RealMain(%s)', upload_args)
1288 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001289 except KeyboardInterrupt:
1290 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 except:
1292 # If we got an exception after the user typed a description for their
1293 # change, back up the description before re-raising.
1294 if change_desc:
1295 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1296 print '\nGot exception while uploading -- saving description to %s\n' \
1297 % backup_path
1298 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001299 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 backup_file.close()
1301 raise
1302
1303 if not cl.GetIssue():
1304 cl.SetIssue(issue)
1305 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001306
1307 if options.use_commit_queue:
1308 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309 return 0
1310
1311
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001312def cleanup_list(l):
1313 """Fixes a list so that comma separated items are put as individual items.
1314
1315 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1316 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1317 """
1318 items = sum((i.split(',') for i in l), [])
1319 stripped_items = (i.strip() for i in items)
1320 return sorted(filter(None, stripped_items))
1321
1322
ukai@chromium.orge8077812012-02-03 03:41:46 +00001323@usage('[args to "git diff"]')
1324def CMDupload(parser, args):
1325 """upload the current changelist to codereview"""
1326 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1327 help='bypass upload presubmit hook')
1328 parser.add_option('-f', action='store_true', dest='force',
1329 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001330 parser.add_option('-m', dest='message', help='message for patchset')
1331 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001332 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001333 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001334 help='reviewer email addresses')
1335 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001336 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001337 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001338 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001339 help='send email to reviewer immediately')
1340 parser.add_option("--emulate_svn_auto_props", action="store_true",
1341 dest="emulate_svn_auto_props",
1342 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001343 parser.add_option('-c', '--use-commit-queue', action='store_true',
1344 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001345 parser.add_option('--target_branch',
1346 help='When uploading to gerrit, remote branch to '
1347 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001348 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001349 (options, args) = parser.parse_args(args)
1350
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001351 if options.target_branch and not settings.GetIsGerrit():
1352 parser.error('Use --target_branch for non gerrit repository.')
1353
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001354 # Print warning if the user used the -m/--message argument. This will soon
1355 # change to -t/--title.
1356 if options.message:
1357 print >> sys.stderr, (
1358 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1359 'In the near future, -m or --message will send a message instead.\n'
1360 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001361
ukai@chromium.org259e4682012-10-25 07:36:33 +00001362 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001363 return 1
1364
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001365 options.reviewers = cleanup_list(options.reviewers)
1366 options.cc = cleanup_list(options.cc)
1367
ukai@chromium.orge8077812012-02-03 03:41:46 +00001368 cl = Changelist()
1369 if args:
1370 # TODO(ukai): is it ok for gerrit case?
1371 base_branch = args[0]
1372 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001373 # Default to diffing against common ancestor of upstream branch
1374 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001375 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001376
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001377 # Apply watchlists on upload.
1378 change = cl.GetChange(base_branch, None)
1379 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1380 files = [f.LocalPath() for f in change.AffectedFiles()]
1381 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1382
ukai@chromium.orge8077812012-02-03 03:41:46 +00001383 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001384 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385 may_prompt=not options.force,
1386 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001387 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001388 if not hook_results.should_continue():
1389 return 1
1390 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001391 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001392
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001393 if cl.GetIssue():
1394 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1395 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001396 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001397 print ('The last upload made from this repository was patchset #%d but '
1398 'the most recent patchset on the server is #%d.'
1399 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001400 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1401 'from another machine or branch the patch you\'re uploading now '
1402 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001403 ask_for_data('About to upload; enter to confirm.')
1404
iannucci@chromium.org79540052012-10-19 23:15:26 +00001405 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001406 if settings.GetIsGerrit():
1407 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001408 ret = RietveldUpload(options, args, cl)
1409 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001410 git_set_branch_value('last-upload-hash',
1411 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001412
1413 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001414
1415
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001416def IsSubmoduleMergeCommit(ref):
1417 # When submodules are added to the repo, we expect there to be a single
1418 # non-git-svn merge commit at remote HEAD with a signature comment.
1419 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001420 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001421 return RunGit(cmd) != ''
1422
1423
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001424def SendUpstream(parser, args, cmd):
1425 """Common code for CmdPush and CmdDCommit
1426
1427 Squashed commit into a single.
1428 Updates changelog with metadata (e.g. pointer to review).
1429 Pushes/dcommits the code upstream.
1430 Updates review and closes.
1431 """
1432 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1433 help='bypass upload presubmit hook')
1434 parser.add_option('-m', dest='message',
1435 help="override review description")
1436 parser.add_option('-f', action='store_true', dest='force',
1437 help="force yes to questions (don't prompt)")
1438 parser.add_option('-c', dest='contributor',
1439 help="external contributor for patch (appended to " +
1440 "description and used as author for git). Should be " +
1441 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001442 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 (options, args) = parser.parse_args(args)
1444 cl = Changelist()
1445
1446 if not args or cmd == 'push':
1447 # Default to merging against our best guess of the upstream branch.
1448 args = [cl.GetUpstreamBranch()]
1449
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001450 if options.contributor:
1451 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1452 print "Please provide contibutor as 'First Last <email@example.com>'"
1453 return 1
1454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001456 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457
ukai@chromium.org259e4682012-10-25 07:36:33 +00001458 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 return 1
1460
1461 # This rev-list syntax means "show all commits not in my branch that
1462 # are in base_branch".
1463 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1464 base_branch]).splitlines()
1465 if upstream_commits:
1466 print ('Base branch "%s" has %d commits '
1467 'not in this branch.' % (base_branch, len(upstream_commits)))
1468 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1469 return 1
1470
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001471 # This is the revision `svn dcommit` will commit on top of.
1472 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1473 '--pretty=format:%H'])
1474
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001476 # If the base_head is a submodule merge commit, the first parent of the
1477 # base_head should be a git-svn commit, which is what we're interested in.
1478 base_svn_head = base_branch
1479 if base_has_submodules:
1480 base_svn_head += '^1'
1481
1482 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483 if extra_commits:
1484 print ('This branch has %d additional commits not upstreamed yet.'
1485 % len(extra_commits.splitlines()))
1486 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1487 'before attempting to %s.' % (base_branch, cmd))
1488 return 1
1489
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001490 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001491 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001492 author = None
1493 if options.contributor:
1494 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001495 hook_results = cl.RunHook(
1496 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001497 may_prompt=not options.force,
1498 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001499 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001500 if not hook_results.should_continue():
1501 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502
1503 if cmd == 'dcommit':
1504 # Check the tree status if the tree status URL is set.
1505 status = GetTreeStatus()
1506 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001507 print('The tree is closed. Please wait for it to reopen. Use '
1508 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001509 return 1
1510 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001511 print('Unable to determine tree status. Please verify manually and '
1512 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001513 else:
1514 breakpad.SendStack(
1515 'GitClHooksBypassedCommit',
1516 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001517 (cl.GetRietveldServer(), cl.GetIssue()),
1518 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001520 change_desc = ChangeDescription(options.message)
1521 if not change_desc.description and cl.GetIssue():
1522 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001523
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001524 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001525 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001526 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001527 else:
1528 print 'No description set.'
1529 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1530 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001532 # Keep a separate copy for the commit message, because the commit message
1533 # contains the link to the Rietveld issue, while the Rietveld message contains
1534 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001535 # Keep a separate copy for the commit message.
1536 if cl.GetIssue():
1537 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1538
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001539 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001540 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001541 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001543 commit_desc.append_footer('Patch from %s.' % options.contributor)
1544
1545 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546
1547 branches = [base_branch, cl.GetBranchRef()]
1548 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001549 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001550 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001552 # We want to squash all this branch's commits into one commit with the proper
1553 # description. We do this by doing a "reset --soft" to the base branch (which
1554 # keeps the working copy the same), then dcommitting that. If origin/master
1555 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1556 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001558 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1559 # Delete the branches if they exist.
1560 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1561 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1562 result = RunGitWithCode(showref_cmd)
1563 if result[0] == 0:
1564 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565
1566 # We might be in a directory that's present in this branch but not in the
1567 # trunk. Move up to the top of the tree so that git commands that expect a
1568 # valid CWD won't fail after we check out the merge branch.
1569 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1570 if rel_base_path:
1571 os.chdir(rel_base_path)
1572
1573 # Stuff our change into the merge branch.
1574 # We wrap in a try...finally block so if anything goes wrong,
1575 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001576 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001578 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1579 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001581 RunGit(
1582 [
1583 'commit', '--author', options.contributor,
1584 '-m', commit_desc.description,
1585 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001586 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001587 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001588 if base_has_submodules:
1589 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1590 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1591 RunGit(['checkout', CHERRY_PICK_BRANCH])
1592 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593 if cmd == 'push':
1594 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001595 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596 retcode, output = RunGitWithCode(
1597 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1598 logging.debug(output)
1599 else:
1600 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001601 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001602 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001603 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 finally:
1605 # And then swap back to the original branch and clean up.
1606 RunGit(['checkout', '-q', cl.GetBranch()])
1607 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001608 if base_has_submodules:
1609 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610
1611 if cl.GetIssue():
1612 if cmd == 'dcommit' and 'Committed r' in output:
1613 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1614 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001615 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1616 for l in output.splitlines(False))
1617 match = filter(None, match)
1618 if len(match) != 1:
1619 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1620 output)
1621 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622 else:
1623 return 1
1624 viewvc_url = settings.GetViewVCUrl()
1625 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001626 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001627 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001628 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001629 print ('Closing issue '
1630 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001631 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001633 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001634 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001635 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001636 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1637 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001638 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001639
1640 if retcode == 0:
1641 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1642 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001643 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645 return 0
1646
1647
1648@usage('[upstream branch to apply against]')
1649def CMDdcommit(parser, args):
1650 """commit the current changelist via git-svn"""
1651 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001652 message = """This doesn't appear to be an SVN repository.
1653If your project has a git mirror with an upstream SVN master, you probably need
1654to run 'git svn init', see your project's git mirror documentation.
1655If your project has a true writeable upstream repository, you probably want
1656to run 'git cl push' instead.
1657Choose wisely, if you get this wrong, your commit might appear to succeed but
1658will instead be silently ignored."""
1659 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001660 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001661 return SendUpstream(parser, args, 'dcommit')
1662
1663
1664@usage('[upstream branch to apply against]')
1665def CMDpush(parser, args):
1666 """commit the current changelist via git"""
1667 if settings.GetIsGitSvn():
1668 print('This appears to be an SVN repository.')
1669 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001670 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001671 return SendUpstream(parser, args, 'push')
1672
1673
1674@usage('<patch url or issue id>')
1675def CMDpatch(parser, args):
1676 """patch in a code review"""
1677 parser.add_option('-b', dest='newbranch',
1678 help='create a new branch off trunk for the patch')
1679 parser.add_option('-f', action='store_true', dest='force',
1680 help='with -b, clobber any existing branch')
1681 parser.add_option('--reject', action='store_true', dest='reject',
1682 help='allow failed patches and spew .rej files')
1683 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1684 help="don't commit after patch applies")
1685 (options, args) = parser.parse_args(args)
1686 if len(args) != 1:
1687 parser.print_help()
1688 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001689 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001691 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001692 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001693
maruel@chromium.org52424302012-08-29 15:14:30 +00001694 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001695 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001696 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001697 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001698 patchset = cl.GetMostRecentPatchset(issue)
1699 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001701 # Assume it's a URL to the patch. Default to https.
1702 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001703 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001704 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001705 DieWithError('Must pass an issue ID or full URL for '
1706 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001707 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001708 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001709 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710
1711 if options.newbranch:
1712 if options.force:
1713 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001714 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001715 RunGit(['checkout', '-b', options.newbranch,
1716 Changelist().GetUpstreamBranch()])
1717
1718 # Switch up to the top-level directory, if necessary, in preparation for
1719 # applying the patch.
1720 top = RunGit(['rev-parse', '--show-cdup']).strip()
1721 if top:
1722 os.chdir(top)
1723
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724 # Git patches have a/ at the beginning of source paths. We strip that out
1725 # with a sed script rather than the -p flag to patch so we can feed either
1726 # Git or svn-style patches into the same apply command.
1727 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001728 try:
1729 patch_data = subprocess2.check_output(
1730 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1731 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732 DieWithError('Git patch mungling failed.')
1733 logging.info(patch_data)
1734 # We use "git apply" to apply the patch instead of "patch" so that we can
1735 # pick up file adds.
1736 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001737 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738 if options.reject:
1739 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001740 try:
1741 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1742 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 DieWithError('Failed to apply the patch')
1744
1745 # If we had an issue, commit the current state and register the issue.
1746 if not options.nocommit:
1747 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1748 cl = Changelist()
1749 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001750 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001751 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001752 else:
1753 print "Patch applied to index."
1754 return 0
1755
1756
1757def CMDrebase(parser, args):
1758 """rebase current branch on top of svn repo"""
1759 # Provide a wrapper for git svn rebase to help avoid accidental
1760 # git svn dcommit.
1761 # It's the only command that doesn't use parser at all since we just defer
1762 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001763 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764
1765
1766def GetTreeStatus():
1767 """Fetches the tree status and returns either 'open', 'closed',
1768 'unknown' or 'unset'."""
1769 url = settings.GetTreeStatusUrl(error_ok=True)
1770 if url:
1771 status = urllib2.urlopen(url).read().lower()
1772 if status.find('closed') != -1 or status == '0':
1773 return 'closed'
1774 elif status.find('open') != -1 or status == '1':
1775 return 'open'
1776 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777 return 'unset'
1778
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001779
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780def GetTreeStatusReason():
1781 """Fetches the tree status from a json url and returns the message
1782 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001783 url = settings.GetTreeStatusUrl()
1784 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001785 connection = urllib2.urlopen(json_url)
1786 status = json.loads(connection.read())
1787 connection.close()
1788 return status['message']
1789
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001790
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791def CMDtree(parser, args):
1792 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001793 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794 status = GetTreeStatus()
1795 if 'unset' == status:
1796 print 'You must configure your tree status URL by running "git cl config".'
1797 return 2
1798
1799 print "The tree is %s" % status
1800 print
1801 print GetTreeStatusReason()
1802 if status != 'open':
1803 return 1
1804 return 0
1805
1806
maruel@chromium.org15192402012-09-06 12:38:29 +00001807def CMDtry(parser, args):
1808 """Triggers a try job through Rietveld."""
1809 group = optparse.OptionGroup(parser, "Try job options")
1810 group.add_option(
1811 "-b", "--bot", action="append",
1812 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1813 "times to specify multiple builders. ex: "
1814 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1815 "the try server waterfall for the builders name and the tests "
1816 "available. Can also be used to specify gtest_filter, e.g. "
1817 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1818 group.add_option(
1819 "-r", "--revision",
1820 help="Revision to use for the try job; default: the "
1821 "revision will be determined by the try server; see "
1822 "its waterfall for more info")
1823 group.add_option(
1824 "-c", "--clobber", action="store_true", default=False,
1825 help="Force a clobber before building; e.g. don't do an "
1826 "incremental build")
1827 group.add_option(
1828 "--project",
1829 help="Override which project to use. Projects are defined "
1830 "server-side to define what default bot set to use")
1831 group.add_option(
1832 "-t", "--testfilter", action="append", default=[],
1833 help=("Apply a testfilter to all the selected builders. Unless the "
1834 "builders configurations are similar, use multiple "
1835 "--bot <builder>:<test> arguments."))
1836 group.add_option(
1837 "-n", "--name", help="Try job name; default to current branch name")
1838 parser.add_option_group(group)
1839 options, args = parser.parse_args(args)
1840
1841 if args:
1842 parser.error('Unknown arguments: %s' % args)
1843
1844 cl = Changelist()
1845 if not cl.GetIssue():
1846 parser.error('Need to upload first')
1847
1848 if not options.name:
1849 options.name = cl.GetBranch()
1850
1851 # Process --bot and --testfilter.
1852 if not options.bot:
1853 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001854 change = cl.GetChange(
1855 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1856 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001857 options.bot = presubmit_support.DoGetTrySlaves(
1858 change,
1859 change.LocalPaths(),
1860 settings.GetRoot(),
1861 None,
1862 None,
1863 options.verbose,
1864 sys.stdout)
1865 if not options.bot:
1866 parser.error('No default try builder to try, use --bot')
1867
1868 builders_and_tests = {}
1869 for bot in options.bot:
1870 if ':' in bot:
1871 builder, tests = bot.split(':', 1)
1872 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1873 elif ',' in bot:
1874 parser.error('Specify one bot per --bot flag')
1875 else:
1876 builders_and_tests.setdefault(bot, []).append('defaulttests')
1877
1878 if options.testfilter:
1879 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1880 builders_and_tests = dict(
1881 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1882 if t != ['compile'])
1883
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001884 if any('triggered' in b for b in builders_and_tests):
1885 print >> sys.stderr, (
1886 'ERROR You are trying to send a job to a triggered bot. This type of'
1887 ' bot requires an\ninitial job from a parent (usually a builder). '
1888 'Instead send your job to the parent.\n'
1889 'Bot list: %s' % builders_and_tests)
1890 return 1
1891
maruel@chromium.org15192402012-09-06 12:38:29 +00001892 patchset = cl.GetPatchset()
1893 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001894 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001895
1896 cl.RpcServer().trigger_try_jobs(
1897 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1898 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001899 print('Tried jobs on:')
1900 length = max(len(builder) for builder in builders_and_tests)
1901 for builder in sorted(builders_and_tests):
1902 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001903 return 0
1904
1905
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001906@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001908 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001909 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001910 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001911 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001912 return 0
1913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001914 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001915 if args:
1916 # One arg means set upstream branch.
1917 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1918 cl = Changelist()
1919 print "Upstream branch set to " + cl.GetUpstreamBranch()
1920 else:
1921 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922 return 0
1923
1924
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001925def CMDset_commit(parser, args):
1926 """set the commit bit"""
1927 _, args = parser.parse_args(args)
1928 if args:
1929 parser.error('Unrecognized args: %s' % ' '.join(args))
1930 cl = Changelist()
1931 cl.SetFlag('commit', '1')
1932 return 0
1933
1934
groby@chromium.org411034a2013-02-26 15:12:01 +00001935def CMDset_close(parser, args):
1936 """close the issue"""
1937 _, args = parser.parse_args(args)
1938 if args:
1939 parser.error('Unrecognized args: %s' % ' '.join(args))
1940 cl = Changelist()
1941 # Ensure there actually is an issue to close.
1942 cl.GetDescription()
1943 cl.CloseIssue()
1944 return 0
1945
1946
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001947def CMDformat(parser, args):
1948 """run clang-format on the diff"""
1949 CLANG_EXTS = ['.cc', '.cpp', '.h']
1950 parser.add_option('--full', action='store_true', default=False)
1951 opts, args = parser.parse_args(args)
1952 if args:
1953 parser.error('Unrecognized args: %s' % ' '.join(args))
1954
digit@chromium.org29e47272013-05-17 17:01:46 +00001955 # Generate diff for the current branch's changes.
1956 diff_cmd = ['diff', '--no-ext-diff']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001957 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00001958 # Only list the names of modified files.
1959 diff_cmd.append('--name-only')
1960 else:
1961 # Only generate context-less patches.
1962 diff_cmd.append('-U0')
1963
1964 # Grab the merge-base commit, i.e. the upstream commit of the current
1965 # branch when it was created or the last time it was rebased. This is
1966 # to cover the case where the user may have called "git fetch origin",
1967 # moving the origin branch to a newer commit, but hasn't rebased yet.
1968 upstream_commit = None
1969 cl = Changelist()
1970 upstream_branch = cl.GetUpstreamBranch()
1971 if upstream_branch:
1972 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
1973 upstream_commit = upstream_commit.strip()
1974
1975 if not upstream_commit:
1976 DieWithError('Could not find base commit for this branch. '
1977 'Are you in detached state?')
1978
1979 diff_cmd.append(upstream_commit)
1980
1981 # Handle source file filtering.
1982 diff_cmd.append('--')
1983 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
1984 diff_output = RunGit(diff_cmd)
1985
1986 if opts.full:
1987 # diff_output is a list of files to send to clang-format.
1988 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001989 if not files:
1990 print "Nothing to format."
1991 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00001992 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001993 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00001994 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001995 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
1996 'clang-format-diff.py')
1997 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00001998 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
1999 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2000 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002001
2002 return 0
2003
2004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002005def Command(name):
2006 return getattr(sys.modules[__name__], 'CMD' + name, None)
2007
2008
2009def CMDhelp(parser, args):
2010 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002011 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002012 if len(args) == 1:
2013 return main(args + ['--help'])
2014 parser.print_help()
2015 return 0
2016
2017
2018def GenUsage(parser, command):
2019 """Modify an OptParse object with the function's documentation."""
2020 obj = Command(command)
2021 more = getattr(obj, 'usage_more', '')
2022 if command == 'help':
2023 command = '<command>'
2024 else:
2025 # OptParser.description prefer nicely non-formatted strings.
2026 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2027 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2028
2029
2030def main(argv):
2031 """Doesn't parse the arguments here, just find the right subcommand to
2032 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002033 if sys.hexversion < 0x02060000:
2034 print >> sys.stderr, (
2035 '\nYour python version %s is unsupported, please upgrade.\n' %
2036 sys.version.split(' ', 1)[0])
2037 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002038
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002039 # Reload settings.
2040 global settings
2041 settings = Settings()
2042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043 # Do it late so all commands are listed.
2044 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
2045 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
2046 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
2047
2048 # Create the option parse and add --verbose support.
2049 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002050 parser.add_option(
2051 '-v', '--verbose', action='count', default=0,
2052 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053 old_parser_args = parser.parse_args
2054 def Parse(args):
2055 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002056 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002057 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002058 elif options.verbose:
2059 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060 else:
2061 logging.basicConfig(level=logging.WARNING)
2062 return options, args
2063 parser.parse_args = Parse
2064
2065 if argv:
2066 command = Command(argv[0])
2067 if command:
2068 # "fix" the usage and the description now that we know the subcommand.
2069 GenUsage(parser, argv[0])
2070 try:
2071 return command(parser, argv[1:])
2072 except urllib2.HTTPError, e:
2073 if e.code != 500:
2074 raise
2075 DieWithError(
2076 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2077 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2078
2079 # Not a known command. Default to help.
2080 GenUsage(parser, 'help')
2081 return CMDhelp(parser, argv)
2082
2083
2084if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002085 # These affect sys.stdout so do it outside of main() to simplify mocks in
2086 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002087 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002088 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089 sys.exit(main(sys.argv[1:]))