blob: 6b735ada00ebde2c155be1871fe03875f2ddcb48 [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
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001123def CMDdescription(parser, args):
1124 """brings up the editor for the current CL's description."""
1125 cl = Changelist()
1126 if not cl.GetIssue():
1127 DieWithError('This branch has no associated changelist.')
1128 description = ChangeDescription(cl.GetDescription())
1129 description.prompt()
1130 cl.UpdateDescription(description.description)
1131 return 0
1132
1133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134def CreateDescriptionFromLog(args):
1135 """Pulls out the commit log to use as a base for the CL description."""
1136 log_args = []
1137 if len(args) == 1 and not args[0].endswith('.'):
1138 log_args = [args[0] + '..']
1139 elif len(args) == 1 and args[0].endswith('...'):
1140 log_args = [args[0][:-1]]
1141 elif len(args) == 2:
1142 log_args = [args[0] + '..' + args[1]]
1143 else:
1144 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001145 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146
1147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148def CMDpresubmit(parser, args):
1149 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001150 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001152 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001153 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 (options, args) = parser.parse_args(args)
1155
ukai@chromium.org259e4682012-10-25 07:36:33 +00001156 if not options.force and is_dirty_git_tree('presubmit'):
1157 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 return 1
1159
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001160 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161 if args:
1162 base_branch = args[0]
1163 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001164 # Default to diffing against the common ancestor of the upstream branch.
1165 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001167 cl.RunHook(
1168 committing=not options.upload,
1169 may_prompt=False,
1170 verbose=options.verbose,
1171 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001172 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
1174
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001175def AddChangeIdToCommitMessage(options, args):
1176 """Re-commits using the current message, assumes the commit hook is in
1177 place.
1178 """
1179 log_desc = options.message or CreateDescriptionFromLog(args)
1180 git_command = ['commit', '--amend', '-m', log_desc]
1181 RunGit(git_command)
1182 new_log_desc = CreateDescriptionFromLog(args)
1183 if CHANGE_ID in new_log_desc:
1184 print 'git-cl: Added Change-Id to commit message.'
1185 else:
1186 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1187
1188
ukai@chromium.orge8077812012-02-03 03:41:46 +00001189def GerritUpload(options, args, cl):
1190 """upload the current branch to gerrit."""
1191 # We assume the remote called "origin" is the one we want.
1192 # It is probably not worthwhile to support different workflows.
1193 remote = 'origin'
1194 branch = 'master'
1195 if options.target_branch:
1196 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001198 change_desc = ChangeDescription(
1199 options.message or CreateDescriptionFromLog(args))
1200 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001201 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001203 if CHANGE_ID not in change_desc.description:
1204 AddChangeIdToCommitMessage(options, args)
1205 if options.reviewers:
1206 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207
ukai@chromium.orge8077812012-02-03 03:41:46 +00001208 receive_options = []
1209 cc = cl.GetCCList().split(',')
1210 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001211 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001212 cc = filter(None, cc)
1213 if cc:
1214 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001215 if change_desc.get_reviewers():
1216 receive_options.extend(
1217 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
ukai@chromium.orge8077812012-02-03 03:41:46 +00001219 git_command = ['push']
1220 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001221 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001222 ' '.join(receive_options))
1223 git_command += [remote, 'HEAD:refs/for/' + branch]
1224 RunGit(git_command)
1225 # TODO(ukai): parse Change-Id: and set issue number?
1226 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228
ukai@chromium.orge8077812012-02-03 03:41:46 +00001229def RietveldUpload(options, args, cl):
1230 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1232 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 if options.emulate_svn_auto_props:
1234 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
1236 change_desc = None
1237
1238 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001239 if options.title:
1240 upload_args.extend(['--title', options.title])
1241 elif options.message:
1242 # TODO(rogerta): for now, the -m option will also set the --title option
1243 # for upload.py. Soon this will be changed to set the --message option.
1244 # Will wait until people are used to typing -t instead of -m.
1245 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001246 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 print ("This branch is associated with issue %s. "
1248 "Adding patch to that issue." % cl.GetIssue())
1249 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001250 if options.title:
1251 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001252 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001253 change_desc = ChangeDescription(message)
1254 if options.reviewers:
1255 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001256 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001257 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001258
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001259 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 print "Description is empty; aborting."
1261 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001262
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001263 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001264 if change_desc.get_reviewers():
1265 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001266 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001267 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001268 DieWithError("Must specify reviewers to send email.")
1269 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001270 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001271 if cc:
1272 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001274 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001275 if not options.find_copies:
1276 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001277
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 # Include the upstream repo's URL in the change -- this is useful for
1279 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001280 remote_url = cl.GetGitBaseUrlFromConfig()
1281 if not remote_url:
1282 if settings.GetIsGitSvn():
1283 # URL is dependent on the current directory.
1284 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1285 if data:
1286 keys = dict(line.split(': ', 1) for line in data.splitlines()
1287 if ': ' in line)
1288 remote_url = keys.get('URL', None)
1289 else:
1290 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1291 remote_url = (cl.GetRemoteUrl() + '@'
1292 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 if remote_url:
1294 upload_args.extend(['--base_url', remote_url])
1295
1296 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001297 upload_args = ['upload'] + upload_args + args
1298 logging.info('upload.RealMain(%s)', upload_args)
1299 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001300 except KeyboardInterrupt:
1301 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 except:
1303 # If we got an exception after the user typed a description for their
1304 # change, back up the description before re-raising.
1305 if change_desc:
1306 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1307 print '\nGot exception while uploading -- saving description to %s\n' \
1308 % backup_path
1309 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001310 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 backup_file.close()
1312 raise
1313
1314 if not cl.GetIssue():
1315 cl.SetIssue(issue)
1316 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001317
1318 if options.use_commit_queue:
1319 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 return 0
1321
1322
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001323def cleanup_list(l):
1324 """Fixes a list so that comma separated items are put as individual items.
1325
1326 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1327 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1328 """
1329 items = sum((i.split(',') for i in l), [])
1330 stripped_items = (i.strip() for i in items)
1331 return sorted(filter(None, stripped_items))
1332
1333
ukai@chromium.orge8077812012-02-03 03:41:46 +00001334@usage('[args to "git diff"]')
1335def CMDupload(parser, args):
1336 """upload the current changelist to codereview"""
1337 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1338 help='bypass upload presubmit hook')
1339 parser.add_option('-f', action='store_true', dest='force',
1340 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001341 parser.add_option('-m', dest='message', help='message for patchset')
1342 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001343 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001344 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001345 help='reviewer email addresses')
1346 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001347 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001348 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001349 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001350 help='send email to reviewer immediately')
1351 parser.add_option("--emulate_svn_auto_props", action="store_true",
1352 dest="emulate_svn_auto_props",
1353 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001354 parser.add_option('-c', '--use-commit-queue', action='store_true',
1355 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001356 parser.add_option('--target_branch',
1357 help='When uploading to gerrit, remote branch to '
1358 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001359 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001360 (options, args) = parser.parse_args(args)
1361
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001362 if options.target_branch and not settings.GetIsGerrit():
1363 parser.error('Use --target_branch for non gerrit repository.')
1364
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001365 # Print warning if the user used the -m/--message argument. This will soon
1366 # change to -t/--title.
1367 if options.message:
1368 print >> sys.stderr, (
1369 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1370 'In the near future, -m or --message will send a message instead.\n'
1371 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001372
ukai@chromium.org259e4682012-10-25 07:36:33 +00001373 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001374 return 1
1375
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001376 options.reviewers = cleanup_list(options.reviewers)
1377 options.cc = cleanup_list(options.cc)
1378
ukai@chromium.orge8077812012-02-03 03:41:46 +00001379 cl = Changelist()
1380 if args:
1381 # TODO(ukai): is it ok for gerrit case?
1382 base_branch = args[0]
1383 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 # Default to diffing against common ancestor of upstream branch
1385 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001386 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001387
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001388 # Apply watchlists on upload.
1389 change = cl.GetChange(base_branch, None)
1390 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1391 files = [f.LocalPath() for f in change.AffectedFiles()]
1392 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1393
ukai@chromium.orge8077812012-02-03 03:41:46 +00001394 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001395 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001396 may_prompt=not options.force,
1397 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001398 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001399 if not hook_results.should_continue():
1400 return 1
1401 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001402 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001403
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001404 if cl.GetIssue():
1405 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1406 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001407 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001408 print ('The last upload made from this repository was patchset #%d but '
1409 'the most recent patchset on the server is #%d.'
1410 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001411 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1412 'from another machine or branch the patch you\'re uploading now '
1413 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001414 ask_for_data('About to upload; enter to confirm.')
1415
iannucci@chromium.org79540052012-10-19 23:15:26 +00001416 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001417 if settings.GetIsGerrit():
1418 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001419 ret = RietveldUpload(options, args, cl)
1420 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001421 git_set_branch_value('last-upload-hash',
1422 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001423
1424 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001425
1426
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001427def IsSubmoduleMergeCommit(ref):
1428 # When submodules are added to the repo, we expect there to be a single
1429 # non-git-svn merge commit at remote HEAD with a signature comment.
1430 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001431 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001432 return RunGit(cmd) != ''
1433
1434
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435def SendUpstream(parser, args, cmd):
1436 """Common code for CmdPush and CmdDCommit
1437
1438 Squashed commit into a single.
1439 Updates changelog with metadata (e.g. pointer to review).
1440 Pushes/dcommits the code upstream.
1441 Updates review and closes.
1442 """
1443 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1444 help='bypass upload presubmit hook')
1445 parser.add_option('-m', dest='message',
1446 help="override review description")
1447 parser.add_option('-f', action='store_true', dest='force',
1448 help="force yes to questions (don't prompt)")
1449 parser.add_option('-c', dest='contributor',
1450 help="external contributor for patch (appended to " +
1451 "description and used as author for git). Should be " +
1452 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001453 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 (options, args) = parser.parse_args(args)
1455 cl = Changelist()
1456
1457 if not args or cmd == 'push':
1458 # Default to merging against our best guess of the upstream branch.
1459 args = [cl.GetUpstreamBranch()]
1460
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001461 if options.contributor:
1462 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1463 print "Please provide contibutor as 'First Last <email@example.com>'"
1464 return 1
1465
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001467 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
ukai@chromium.org259e4682012-10-25 07:36:33 +00001469 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 return 1
1471
1472 # This rev-list syntax means "show all commits not in my branch that
1473 # are in base_branch".
1474 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1475 base_branch]).splitlines()
1476 if upstream_commits:
1477 print ('Base branch "%s" has %d commits '
1478 'not in this branch.' % (base_branch, len(upstream_commits)))
1479 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1480 return 1
1481
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001482 # This is the revision `svn dcommit` will commit on top of.
1483 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1484 '--pretty=format:%H'])
1485
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001487 # If the base_head is a submodule merge commit, the first parent of the
1488 # base_head should be a git-svn commit, which is what we're interested in.
1489 base_svn_head = base_branch
1490 if base_has_submodules:
1491 base_svn_head += '^1'
1492
1493 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001494 if extra_commits:
1495 print ('This branch has %d additional commits not upstreamed yet.'
1496 % len(extra_commits.splitlines()))
1497 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1498 'before attempting to %s.' % (base_branch, cmd))
1499 return 1
1500
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001501 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001502 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001503 author = None
1504 if options.contributor:
1505 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001506 hook_results = cl.RunHook(
1507 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001508 may_prompt=not options.force,
1509 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001510 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001511 if not hook_results.should_continue():
1512 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513
1514 if cmd == 'dcommit':
1515 # Check the tree status if the tree status URL is set.
1516 status = GetTreeStatus()
1517 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001518 print('The tree is closed. Please wait for it to reopen. Use '
1519 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520 return 1
1521 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001522 print('Unable to determine tree status. Please verify manually and '
1523 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001524 else:
1525 breakpad.SendStack(
1526 'GitClHooksBypassedCommit',
1527 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001528 (cl.GetRietveldServer(), cl.GetIssue()),
1529 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001531 change_desc = ChangeDescription(options.message)
1532 if not change_desc.description and cl.GetIssue():
1533 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001535 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001536 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001538 else:
1539 print 'No description set.'
1540 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1541 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001543 # Keep a separate copy for the commit message, because the commit message
1544 # contains the link to the Rietveld issue, while the Rietveld message contains
1545 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001546 # Keep a separate copy for the commit message.
1547 if cl.GetIssue():
1548 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1549
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001550 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001551 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001552 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001553 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001554 commit_desc.append_footer('Patch from %s.' % options.contributor)
1555
1556 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557
1558 branches = [base_branch, cl.GetBranchRef()]
1559 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001560 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001561 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001563 # We want to squash all this branch's commits into one commit with the proper
1564 # description. We do this by doing a "reset --soft" to the base branch (which
1565 # keeps the working copy the same), then dcommitting that. If origin/master
1566 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1567 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001568 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001569 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1570 # Delete the branches if they exist.
1571 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1572 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1573 result = RunGitWithCode(showref_cmd)
1574 if result[0] == 0:
1575 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576
1577 # We might be in a directory that's present in this branch but not in the
1578 # trunk. Move up to the top of the tree so that git commands that expect a
1579 # valid CWD won't fail after we check out the merge branch.
1580 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1581 if rel_base_path:
1582 os.chdir(rel_base_path)
1583
1584 # Stuff our change into the merge branch.
1585 # We wrap in a try...finally block so if anything goes wrong,
1586 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001587 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001589 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1590 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001592 RunGit(
1593 [
1594 'commit', '--author', options.contributor,
1595 '-m', commit_desc.description,
1596 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001598 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001599 if base_has_submodules:
1600 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1601 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1602 RunGit(['checkout', CHERRY_PICK_BRANCH])
1603 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 if cmd == 'push':
1605 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001606 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607 retcode, output = RunGitWithCode(
1608 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1609 logging.debug(output)
1610 else:
1611 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001612 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001613 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001614 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001615 finally:
1616 # And then swap back to the original branch and clean up.
1617 RunGit(['checkout', '-q', cl.GetBranch()])
1618 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001619 if base_has_submodules:
1620 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621
1622 if cl.GetIssue():
1623 if cmd == 'dcommit' and 'Committed r' in output:
1624 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1625 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001626 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1627 for l in output.splitlines(False))
1628 match = filter(None, match)
1629 if len(match) != 1:
1630 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1631 output)
1632 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001633 else:
1634 return 1
1635 viewvc_url = settings.GetViewVCUrl()
1636 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001637 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001638 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001639 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 print ('Closing issue '
1641 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001642 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001644 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001645 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001646 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001647 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1648 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001650
1651 if retcode == 0:
1652 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1653 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001654 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656 return 0
1657
1658
1659@usage('[upstream branch to apply against]')
1660def CMDdcommit(parser, args):
1661 """commit the current changelist via git-svn"""
1662 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001663 message = """This doesn't appear to be an SVN repository.
1664If your project has a git mirror with an upstream SVN master, you probably need
1665to run 'git svn init', see your project's git mirror documentation.
1666If your project has a true writeable upstream repository, you probably want
1667to run 'git cl push' instead.
1668Choose wisely, if you get this wrong, your commit might appear to succeed but
1669will instead be silently ignored."""
1670 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001671 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672 return SendUpstream(parser, args, 'dcommit')
1673
1674
1675@usage('[upstream branch to apply against]')
1676def CMDpush(parser, args):
1677 """commit the current changelist via git"""
1678 if settings.GetIsGitSvn():
1679 print('This appears to be an SVN repository.')
1680 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001681 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 return SendUpstream(parser, args, 'push')
1683
1684
1685@usage('<patch url or issue id>')
1686def CMDpatch(parser, args):
1687 """patch in a code review"""
1688 parser.add_option('-b', dest='newbranch',
1689 help='create a new branch off trunk for the patch')
1690 parser.add_option('-f', action='store_true', dest='force',
1691 help='with -b, clobber any existing branch')
1692 parser.add_option('--reject', action='store_true', dest='reject',
1693 help='allow failed patches and spew .rej files')
1694 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1695 help="don't commit after patch applies")
1696 (options, args) = parser.parse_args(args)
1697 if len(args) != 1:
1698 parser.print_help()
1699 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001700 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001702 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001703 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001704
maruel@chromium.org52424302012-08-29 15:14:30 +00001705 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001707 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001708 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001709 patchset = cl.GetMostRecentPatchset(issue)
1710 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001711 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001712 # Assume it's a URL to the patch. Default to https.
1713 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001714 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001715 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716 DieWithError('Must pass an issue ID or full URL for '
1717 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001718 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001719 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001720 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001721
1722 if options.newbranch:
1723 if options.force:
1724 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001725 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 RunGit(['checkout', '-b', options.newbranch,
1727 Changelist().GetUpstreamBranch()])
1728
1729 # Switch up to the top-level directory, if necessary, in preparation for
1730 # applying the patch.
1731 top = RunGit(['rev-parse', '--show-cdup']).strip()
1732 if top:
1733 os.chdir(top)
1734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 # Git patches have a/ at the beginning of source paths. We strip that out
1736 # with a sed script rather than the -p flag to patch so we can feed either
1737 # Git or svn-style patches into the same apply command.
1738 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001739 try:
1740 patch_data = subprocess2.check_output(
1741 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1742 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 DieWithError('Git patch mungling failed.')
1744 logging.info(patch_data)
1745 # We use "git apply" to apply the patch instead of "patch" so that we can
1746 # pick up file adds.
1747 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001748 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749 if options.reject:
1750 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001751 try:
1752 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1753 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754 DieWithError('Failed to apply the patch')
1755
1756 # If we had an issue, commit the current state and register the issue.
1757 if not options.nocommit:
1758 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1759 cl = Changelist()
1760 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001761 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001762 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763 else:
1764 print "Patch applied to index."
1765 return 0
1766
1767
1768def CMDrebase(parser, args):
1769 """rebase current branch on top of svn repo"""
1770 # Provide a wrapper for git svn rebase to help avoid accidental
1771 # git svn dcommit.
1772 # It's the only command that doesn't use parser at all since we just defer
1773 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001774 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001775
1776
1777def GetTreeStatus():
1778 """Fetches the tree status and returns either 'open', 'closed',
1779 'unknown' or 'unset'."""
1780 url = settings.GetTreeStatusUrl(error_ok=True)
1781 if url:
1782 status = urllib2.urlopen(url).read().lower()
1783 if status.find('closed') != -1 or status == '0':
1784 return 'closed'
1785 elif status.find('open') != -1 or status == '1':
1786 return 'open'
1787 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788 return 'unset'
1789
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001790
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791def GetTreeStatusReason():
1792 """Fetches the tree status from a json url and returns the message
1793 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001794 url = settings.GetTreeStatusUrl()
1795 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 connection = urllib2.urlopen(json_url)
1797 status = json.loads(connection.read())
1798 connection.close()
1799 return status['message']
1800
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001801
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001802def CMDtree(parser, args):
1803 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001804 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805 status = GetTreeStatus()
1806 if 'unset' == status:
1807 print 'You must configure your tree status URL by running "git cl config".'
1808 return 2
1809
1810 print "The tree is %s" % status
1811 print
1812 print GetTreeStatusReason()
1813 if status != 'open':
1814 return 1
1815 return 0
1816
1817
maruel@chromium.org15192402012-09-06 12:38:29 +00001818def CMDtry(parser, args):
1819 """Triggers a try job through Rietveld."""
1820 group = optparse.OptionGroup(parser, "Try job options")
1821 group.add_option(
1822 "-b", "--bot", action="append",
1823 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1824 "times to specify multiple builders. ex: "
1825 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1826 "the try server waterfall for the builders name and the tests "
1827 "available. Can also be used to specify gtest_filter, e.g. "
1828 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1829 group.add_option(
1830 "-r", "--revision",
1831 help="Revision to use for the try job; default: the "
1832 "revision will be determined by the try server; see "
1833 "its waterfall for more info")
1834 group.add_option(
1835 "-c", "--clobber", action="store_true", default=False,
1836 help="Force a clobber before building; e.g. don't do an "
1837 "incremental build")
1838 group.add_option(
1839 "--project",
1840 help="Override which project to use. Projects are defined "
1841 "server-side to define what default bot set to use")
1842 group.add_option(
1843 "-t", "--testfilter", action="append", default=[],
1844 help=("Apply a testfilter to all the selected builders. Unless the "
1845 "builders configurations are similar, use multiple "
1846 "--bot <builder>:<test> arguments."))
1847 group.add_option(
1848 "-n", "--name", help="Try job name; default to current branch name")
1849 parser.add_option_group(group)
1850 options, args = parser.parse_args(args)
1851
1852 if args:
1853 parser.error('Unknown arguments: %s' % args)
1854
1855 cl = Changelist()
1856 if not cl.GetIssue():
1857 parser.error('Need to upload first')
1858
1859 if not options.name:
1860 options.name = cl.GetBranch()
1861
1862 # Process --bot and --testfilter.
1863 if not options.bot:
1864 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001865 change = cl.GetChange(
1866 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1867 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001868 options.bot = presubmit_support.DoGetTrySlaves(
1869 change,
1870 change.LocalPaths(),
1871 settings.GetRoot(),
1872 None,
1873 None,
1874 options.verbose,
1875 sys.stdout)
1876 if not options.bot:
1877 parser.error('No default try builder to try, use --bot')
1878
1879 builders_and_tests = {}
1880 for bot in options.bot:
1881 if ':' in bot:
1882 builder, tests = bot.split(':', 1)
1883 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1884 elif ',' in bot:
1885 parser.error('Specify one bot per --bot flag')
1886 else:
1887 builders_and_tests.setdefault(bot, []).append('defaulttests')
1888
1889 if options.testfilter:
1890 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1891 builders_and_tests = dict(
1892 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1893 if t != ['compile'])
1894
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001895 if any('triggered' in b for b in builders_and_tests):
1896 print >> sys.stderr, (
1897 'ERROR You are trying to send a job to a triggered bot. This type of'
1898 ' bot requires an\ninitial job from a parent (usually a builder). '
1899 'Instead send your job to the parent.\n'
1900 'Bot list: %s' % builders_and_tests)
1901 return 1
1902
maruel@chromium.org15192402012-09-06 12:38:29 +00001903 patchset = cl.GetPatchset()
1904 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001905 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001906
1907 cl.RpcServer().trigger_try_jobs(
1908 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1909 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001910 print('Tried jobs on:')
1911 length = max(len(builder) for builder in builders_and_tests)
1912 for builder in sorted(builders_and_tests):
1913 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001914 return 0
1915
1916
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001917@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001919 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001920 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001921 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001922 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001923 return 0
1924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001926 if args:
1927 # One arg means set upstream branch.
1928 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1929 cl = Changelist()
1930 print "Upstream branch set to " + cl.GetUpstreamBranch()
1931 else:
1932 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 return 0
1934
1935
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001936def CMDset_commit(parser, args):
1937 """set the commit bit"""
1938 _, args = parser.parse_args(args)
1939 if args:
1940 parser.error('Unrecognized args: %s' % ' '.join(args))
1941 cl = Changelist()
1942 cl.SetFlag('commit', '1')
1943 return 0
1944
1945
groby@chromium.org411034a2013-02-26 15:12:01 +00001946def CMDset_close(parser, args):
1947 """close the issue"""
1948 _, args = parser.parse_args(args)
1949 if args:
1950 parser.error('Unrecognized args: %s' % ' '.join(args))
1951 cl = Changelist()
1952 # Ensure there actually is an issue to close.
1953 cl.GetDescription()
1954 cl.CloseIssue()
1955 return 0
1956
1957
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001958def CMDformat(parser, args):
1959 """run clang-format on the diff"""
1960 CLANG_EXTS = ['.cc', '.cpp', '.h']
1961 parser.add_option('--full', action='store_true', default=False)
1962 opts, args = parser.parse_args(args)
1963 if args:
1964 parser.error('Unrecognized args: %s' % ' '.join(args))
1965
digit@chromium.org29e47272013-05-17 17:01:46 +00001966 # Generate diff for the current branch's changes.
1967 diff_cmd = ['diff', '--no-ext-diff']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001968 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00001969 # Only list the names of modified files.
1970 diff_cmd.append('--name-only')
1971 else:
1972 # Only generate context-less patches.
1973 diff_cmd.append('-U0')
1974
1975 # Grab the merge-base commit, i.e. the upstream commit of the current
1976 # branch when it was created or the last time it was rebased. This is
1977 # to cover the case where the user may have called "git fetch origin",
1978 # moving the origin branch to a newer commit, but hasn't rebased yet.
1979 upstream_commit = None
1980 cl = Changelist()
1981 upstream_branch = cl.GetUpstreamBranch()
1982 if upstream_branch:
1983 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
1984 upstream_commit = upstream_commit.strip()
1985
1986 if not upstream_commit:
1987 DieWithError('Could not find base commit for this branch. '
1988 'Are you in detached state?')
1989
1990 diff_cmd.append(upstream_commit)
1991
1992 # Handle source file filtering.
1993 diff_cmd.append('--')
1994 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
1995 diff_output = RunGit(diff_cmd)
1996
1997 if opts.full:
1998 # diff_output is a list of files to send to clang-format.
1999 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002000 if not files:
2001 print "Nothing to format."
2002 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002003 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002004 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002005 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002006 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2007 'clang-format-diff.py')
2008 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002009 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2010 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2011 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002012
2013 return 0
2014
2015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016def Command(name):
2017 return getattr(sys.modules[__name__], 'CMD' + name, None)
2018
2019
2020def CMDhelp(parser, args):
2021 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002022 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 if len(args) == 1:
2024 return main(args + ['--help'])
2025 parser.print_help()
2026 return 0
2027
2028
2029def GenUsage(parser, command):
2030 """Modify an OptParse object with the function's documentation."""
2031 obj = Command(command)
2032 more = getattr(obj, 'usage_more', '')
2033 if command == 'help':
2034 command = '<command>'
2035 else:
2036 # OptParser.description prefer nicely non-formatted strings.
2037 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2038 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2039
2040
2041def main(argv):
2042 """Doesn't parse the arguments here, just find the right subcommand to
2043 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002044 if sys.hexversion < 0x02060000:
2045 print >> sys.stderr, (
2046 '\nYour python version %s is unsupported, please upgrade.\n' %
2047 sys.version.split(' ', 1)[0])
2048 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002049
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002050 # Reload settings.
2051 global settings
2052 settings = Settings()
2053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054 # Do it late so all commands are listed.
2055 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
2056 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
2057 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
2058
2059 # Create the option parse and add --verbose support.
2060 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002061 parser.add_option(
2062 '-v', '--verbose', action='count', default=0,
2063 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064 old_parser_args = parser.parse_args
2065 def Parse(args):
2066 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002067 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002069 elif options.verbose:
2070 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071 else:
2072 logging.basicConfig(level=logging.WARNING)
2073 return options, args
2074 parser.parse_args = Parse
2075
2076 if argv:
2077 command = Command(argv[0])
2078 if command:
2079 # "fix" the usage and the description now that we know the subcommand.
2080 GenUsage(parser, argv[0])
2081 try:
2082 return command(parser, argv[1:])
2083 except urllib2.HTTPError, e:
2084 if e.code != 500:
2085 raise
2086 DieWithError(
2087 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2088 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2089
2090 # Not a known command. Default to help.
2091 GenUsage(parser, 'help')
2092 return CMDhelp(parser, argv)
2093
2094
2095if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002096 # These affect sys.stdout so do it outside of main() to simplify mocks in
2097 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002098 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002099 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002100 sys.exit(main(sys.argv[1:]))