blob: 01fe0767afdb4691e94a121b001e30d50b2239ba [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000027from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000028from third_party import upload
29import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000030import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000031import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000033import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000035import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import watchlists
37
38
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000039DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000040POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000042GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000043CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000044
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045# Shortcut since it quickly becomes redundant.
46Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000047
maruel@chromium.orgddd59412011-11-30 14:20:38 +000048# Initialized in main()
49settings = None
50
51
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000053 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054 sys.exit(1)
55
56
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000059 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000060 except subprocess2.CalledProcessError as e:
61 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000064 'Command "%s" failed.\n%s' % (
65 ' '.join(args), error_message or e.stdout or ''))
66 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
68
69def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 """Returns stdout."""
bratell@opera.comf267b0e2013-05-02 09:11:43 +000071 return RunCommand(['git', '--no-pager'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
73
74def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000075 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000076 try:
bratell@opera.comf267b0e2013-05-02 09:11:43 +000077 out, code = subprocess2.communicate(['git', '--no-pager'] + args,
78 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000079 return code, out[0]
80 except ValueError:
81 # When the subprocess fails, it returns None. That triggers a ValueError
82 # when trying to unpack the return value into (out, code).
83 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084
85
86def usage(more):
87 def hook(fn):
88 fn.usage_more = more
89 return fn
90 return hook
91
92
maruel@chromium.org90541732011-04-01 17:54:18 +000093def ask_for_data(prompt):
94 try:
95 return raw_input(prompt)
96 except KeyboardInterrupt:
97 # Hide the exception.
98 sys.exit(1)
99
100
iannucci@chromium.org79540052012-10-19 23:15:26 +0000101def git_set_branch_value(key, value):
102 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000103 if not branch:
104 return
105
106 cmd = ['config']
107 if isinstance(value, int):
108 cmd.append('--int')
109 git_key = 'branch.%s.%s' % (branch, key)
110 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000111
112
113def git_get_branch_default(key, default):
114 branch = Changelist().GetBranch()
115 if branch:
116 git_key = 'branch.%s.%s' % (branch, key)
117 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
118 try:
119 return int(stdout.strip())
120 except ValueError:
121 pass
122 return default
123
124
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000125def add_git_similarity(parser):
126 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000127 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000128 help='Sets the percentage that a pair of files need to match in order to'
129 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000130 parser.add_option(
131 '--find-copies', action='store_true',
132 help='Allows git to look for copies.')
133 parser.add_option(
134 '--no-find-copies', action='store_false', dest='find_copies',
135 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000136
137 old_parser_args = parser.parse_args
138 def Parse(args):
139 options, args = old_parser_args(args)
140
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000141 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000143 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000144 print('Note: Saving similarity of %d%% in git config.'
145 % options.similarity)
146 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000147
iannucci@chromium.org79540052012-10-19 23:15:26 +0000148 options.similarity = max(0, min(options.similarity, 100))
149
150 if options.find_copies is None:
151 options.find_copies = bool(
152 git_get_branch_default('git-find-copies', True))
153 else:
154 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155
156 print('Using %d%% similarity for rename/copy detection. '
157 'Override with --similarity.' % options.similarity)
158
159 return options, args
160 parser.parse_args = Parse
161
162
ukai@chromium.org259e4682012-10-25 07:36:33 +0000163def is_dirty_git_tree(cmd):
164 # Make sure index is up-to-date before running diff-index.
165 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
166 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
167 if dirty:
168 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
169 print 'Uncommitted files: (git diff-index --name-status HEAD)'
170 print dirty[:4096]
171 if len(dirty) > 4096:
172 print '... (run "git diff-index --name-status HEAD" to see full output).'
173 return True
174 return False
175
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000176
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000177def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
178 """Return the corresponding git ref if |base_url| together with |glob_spec|
179 matches the full |url|.
180
181 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
182 """
183 fetch_suburl, as_ref = glob_spec.split(':')
184 if allow_wildcards:
185 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
186 if glob_match:
187 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
188 # "branches/{472,597,648}/src:refs/remotes/svn/*".
189 branch_re = re.escape(base_url)
190 if glob_match.group(1):
191 branch_re += '/' + re.escape(glob_match.group(1))
192 wildcard = glob_match.group(2)
193 if wildcard == '*':
194 branch_re += '([^/]*)'
195 else:
196 # Escape and replace surrounding braces with parentheses and commas
197 # with pipe symbols.
198 wildcard = re.escape(wildcard)
199 wildcard = re.sub('^\\\\{', '(', wildcard)
200 wildcard = re.sub('\\\\,', '|', wildcard)
201 wildcard = re.sub('\\\\}$', ')', wildcard)
202 branch_re += wildcard
203 if glob_match.group(3):
204 branch_re += re.escape(glob_match.group(3))
205 match = re.match(branch_re, url)
206 if match:
207 return re.sub('\*$', match.group(1), as_ref)
208
209 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
210 if fetch_suburl:
211 full_url = base_url + '/' + fetch_suburl
212 else:
213 full_url = base_url
214 if full_url == url:
215 return as_ref
216 return None
217
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000218
iannucci@chromium.org79540052012-10-19 23:15:26 +0000219def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000220 """Prints statistics about the change to the user."""
221 # --no-ext-diff is broken in some versions of Git, so try to work around
222 # this by overriding the environment (but there is still a problem if the
223 # git config key "diff.external" is used).
224 env = os.environ.copy()
225 if 'GIT_EXTERNAL_DIFF' in env:
226 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000227
228 if find_copies:
229 similarity_options = ['--find-copies-harder', '-l100000',
230 '-C%s' % similarity]
231 else:
232 similarity_options = ['-M%s' % similarity]
233
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000234 return subprocess2.call(
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000235 ['git', '--no-pager',
236 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000237 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000238
239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000240class Settings(object):
241 def __init__(self):
242 self.default_server = None
243 self.cc = None
244 self.root = None
245 self.is_git_svn = None
246 self.svn_branch = None
247 self.tree_status_url = None
248 self.viewvc_url = None
249 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000250 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000251 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000252
253 def LazyUpdateIfNeeded(self):
254 """Updates the settings from a codereview.settings file, if available."""
255 if not self.updated:
256 cr_settings_file = FindCodereviewSettingsFile()
257 if cr_settings_file:
258 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000259 self.updated = True
260 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000261 self.updated = True
262
263 def GetDefaultServerUrl(self, error_ok=False):
264 if not self.default_server:
265 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000266 self.default_server = gclient_utils.UpgradeToHttps(
267 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268 if error_ok:
269 return self.default_server
270 if not self.default_server:
271 error_message = ('Could not find settings file. You must configure '
272 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000273 self.default_server = gclient_utils.UpgradeToHttps(
274 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 return self.default_server
276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 def GetRoot(self):
278 if not self.root:
279 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
280 return self.root
281
282 def GetIsGitSvn(self):
283 """Return true if this repo looks like it's using git-svn."""
284 if self.is_git_svn is None:
285 # If you have any "svn-remote.*" config keys, we think you're using svn.
286 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000287 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288 return self.is_git_svn
289
290 def GetSVNBranch(self):
291 if self.svn_branch is None:
292 if not self.GetIsGitSvn():
293 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
294
295 # Try to figure out which remote branch we're based on.
296 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000297 # 1) iterate through our branch history and find the svn URL.
298 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299
300 # regexp matching the git-svn line that contains the URL.
301 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
302
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000303 # We don't want to go through all of history, so read a line from the
304 # pipe at a time.
305 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000306 cmd = ['git', '--no-pager', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000307 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000308 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000309 for line in proc.stdout:
310 match = git_svn_re.match(line)
311 if match:
312 url = match.group(1)
313 proc.stdout.close() # Cut pipe.
314 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000315
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000316 if url:
317 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
318 remotes = RunGit(['config', '--get-regexp',
319 r'^svn-remote\..*\.url']).splitlines()
320 for remote in remotes:
321 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000323 remote = match.group(1)
324 base_url = match.group(2)
325 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000326 ['config', 'svn-remote.%s.fetch' % remote],
327 error_ok=True).strip()
328 if fetch_spec:
329 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
330 if self.svn_branch:
331 break
332 branch_spec = RunGit(
333 ['config', 'svn-remote.%s.branches' % remote],
334 error_ok=True).strip()
335 if branch_spec:
336 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
337 if self.svn_branch:
338 break
339 tag_spec = RunGit(
340 ['config', 'svn-remote.%s.tags' % remote],
341 error_ok=True).strip()
342 if tag_spec:
343 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
344 if self.svn_branch:
345 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000346
347 if not self.svn_branch:
348 DieWithError('Can\'t guess svn branch -- try specifying it on the '
349 'command line')
350
351 return self.svn_branch
352
353 def GetTreeStatusUrl(self, error_ok=False):
354 if not self.tree_status_url:
355 error_message = ('You must configure your tree status URL by running '
356 '"git cl config".')
357 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
358 error_ok=error_ok,
359 error_message=error_message)
360 return self.tree_status_url
361
362 def GetViewVCUrl(self):
363 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000364 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365 return self.viewvc_url
366
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000367 def GetDefaultCCList(self):
368 return self._GetConfig('rietveld.cc', error_ok=True)
369
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000370 def GetDefaultPrivateFlag(self):
371 return self._GetConfig('rietveld.private', error_ok=True)
372
ukai@chromium.orge8077812012-02-03 03:41:46 +0000373 def GetIsGerrit(self):
374 """Return true if this repo is assosiated with gerrit code review system."""
375 if self.is_gerrit is None:
376 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
377 return self.is_gerrit
378
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000379 def GetGitEditor(self):
380 """Return the editor specified in the git config, or None if none is."""
381 if self.git_editor is None:
382 self.git_editor = self._GetConfig('core.editor', error_ok=True)
383 return self.git_editor or None
384
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000385 def _GetConfig(self, param, **kwargs):
386 self.LazyUpdateIfNeeded()
387 return RunGit(['config', param], **kwargs).strip()
388
389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000390def ShortBranchName(branch):
391 """Convert a name like 'refs/heads/foo' to just 'foo'."""
392 return branch.replace('refs/heads/', '')
393
394
395class Changelist(object):
396 def __init__(self, branchref=None):
397 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000398 global settings
399 if not settings:
400 # Happens when git_cl.py is used as a utility library.
401 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 settings.GetDefaultServerUrl()
403 self.branchref = branchref
404 if self.branchref:
405 self.branch = ShortBranchName(self.branchref)
406 else:
407 self.branch = None
408 self.rietveld_server = None
409 self.upstream_branch = None
410 self.has_issue = False
411 self.issue = None
412 self.has_description = False
413 self.description = None
414 self.has_patchset = False
415 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000416 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000417 self.cc = None
418 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000419 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000420
421 def GetCCList(self):
422 """Return the users cc'd on this CL.
423
424 Return is a string suitable for passing to gcl with the --cc flag.
425 """
426 if self.cc is None:
427 base_cc = settings .GetDefaultCCList()
428 more_cc = ','.join(self.watchers)
429 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
430 return self.cc
431
432 def SetWatchers(self, watchers):
433 """Set the list of email addresses that should be cc'd based on the changed
434 files in this CL.
435 """
436 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437
438 def GetBranch(self):
439 """Returns the short branch name, e.g. 'master'."""
440 if not self.branch:
441 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
442 self.branch = ShortBranchName(self.branchref)
443 return self.branch
444
445 def GetBranchRef(self):
446 """Returns the full branch name, e.g. 'refs/heads/master'."""
447 self.GetBranch() # Poke the lazy loader.
448 return self.branchref
449
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000450 @staticmethod
451 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 """Returns a tuple containg remote and remote ref,
453 e.g. 'origin', 'refs/heads/master'
454 """
455 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
457 error_ok=True).strip()
458 if upstream_branch:
459 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
460 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000461 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
462 error_ok=True).strip()
463 if upstream_branch:
464 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000466 # Fall back on trying a git-svn upstream branch.
467 if settings.GetIsGitSvn():
468 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000470 # Else, try to guess the origin remote.
471 remote_branches = RunGit(['branch', '-r']).split()
472 if 'origin/master' in remote_branches:
473 # Fall back on origin/master if it exits.
474 remote = 'origin'
475 upstream_branch = 'refs/heads/master'
476 elif 'origin/trunk' in remote_branches:
477 # Fall back on origin/trunk if it exists. Generally a shared
478 # git-svn clone
479 remote = 'origin'
480 upstream_branch = 'refs/heads/trunk'
481 else:
482 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000483Either pass complete "git diff"-style arguments, like
484 git cl upload origin/master
485or verify this branch is set up to track another (via the --track argument to
486"git checkout -b ...").""")
487
488 return remote, upstream_branch
489
490 def GetUpstreamBranch(self):
491 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000492 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 if remote is not '.':
494 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
495 self.upstream_branch = upstream_branch
496 return self.upstream_branch
497
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000498 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000499 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000500 remote, branch = None, self.GetBranch()
501 seen_branches = set()
502 while branch not in seen_branches:
503 seen_branches.add(branch)
504 remote, branch = self.FetchUpstreamTuple(branch)
505 branch = ShortBranchName(branch)
506 if remote != '.' or branch.startswith('refs/remotes'):
507 break
508 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000509 remotes = RunGit(['remote'], error_ok=True).split()
510 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000511 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000512 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000513 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000514 logging.warning('Could not determine which remote this change is '
515 'associated with, so defaulting to "%s". This may '
516 'not be what you want. You may prevent this message '
517 'by running "git svn info" as documented here: %s',
518 self._remote,
519 GIT_INSTRUCTIONS_URL)
520 else:
521 logging.warn('Could not determine which remote this change is '
522 'associated with. You may prevent this message by '
523 'running "git svn info" as documented here: %s',
524 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000525 branch = 'HEAD'
526 if branch.startswith('refs/remotes'):
527 self._remote = (remote, branch)
528 else:
529 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000530 return self._remote
531
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000532 def GitSanityChecks(self, upstream_git_obj):
533 """Checks git repo status and ensures diff is from local commits."""
534
535 # Verify the commit we're diffing against is in our current branch.
536 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
537 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
538 if upstream_sha != common_ancestor:
539 print >> sys.stderr, (
540 'ERROR: %s is not in the current branch. You may need to rebase '
541 'your tracking branch' % upstream_sha)
542 return False
543
544 # List the commits inside the diff, and verify they are all local.
545 commits_in_diff = RunGit(
546 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
547 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
548 remote_branch = remote_branch.strip()
549 if code != 0:
550 _, remote_branch = self.GetRemoteBranch()
551
552 commits_in_remote = RunGit(
553 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
554
555 common_commits = set(commits_in_diff) & set(commits_in_remote)
556 if common_commits:
557 print >> sys.stderr, (
558 'ERROR: Your diff contains %d commits already in %s.\n'
559 'Run "git log --oneline %s..HEAD" to get a list of commits in '
560 'the diff. If you are using a custom git flow, you can override'
561 ' the reference used for this check with "git config '
562 'gitcl.remotebranch <git-ref>".' % (
563 len(common_commits), remote_branch, upstream_git_obj))
564 return False
565 return True
566
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000567 def GetGitBaseUrlFromConfig(self):
568 """Return the configured base URL from branch.<branchname>.baseurl.
569
570 Returns None if it is not set.
571 """
572 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
573 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 def GetRemoteUrl(self):
576 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
577
578 Returns None if there is no remote.
579 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000580 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000581 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
582
583 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000584 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
587 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000588 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589 else:
590 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591 self.has_issue = True
592 return self.issue
593
594 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000595 if not self.rietveld_server:
596 # If we're on a branch then get the server potentially associated
597 # with that branch.
598 if self.GetIssue():
599 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
600 ['config', self._RietveldServer()], error_ok=True).strip())
601 if not self.rietveld_server:
602 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 return self.rietveld_server
604
605 def GetIssueURL(self):
606 """Get the URL for a particular issue."""
607 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
608
609 def GetDescription(self, pretty=False):
610 if not self.has_description:
611 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000612 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000613 try:
614 self.description = self.RpcServer().get_description(issue).strip()
615 except urllib2.HTTPError, e:
616 if e.code == 404:
617 DieWithError(
618 ('\nWhile fetching the description for issue %d, received a '
619 '404 (not found)\n'
620 'error. It is likely that you deleted this '
621 'issue on the server. If this is the\n'
622 'case, please run\n\n'
623 ' git cl issue 0\n\n'
624 'to clear the association with the deleted issue. Then run '
625 'this command again.') % issue)
626 else:
627 DieWithError(
628 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 self.has_description = True
630 if pretty:
631 wrapper = textwrap.TextWrapper()
632 wrapper.initial_indent = wrapper.subsequent_indent = ' '
633 return wrapper.fill(self.description)
634 return self.description
635
636 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000637 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 if not self.has_patchset:
639 patchset = RunGit(['config', self._PatchsetSetting()],
640 error_ok=True).strip()
641 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000642 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 else:
644 self.patchset = None
645 self.has_patchset = True
646 return self.patchset
647
648 def SetPatchset(self, patchset):
649 """Set this branch's patchset. If patchset=0, clears the patchset."""
650 if patchset:
651 RunGit(['config', self._PatchsetSetting(), str(patchset)])
652 else:
653 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000654 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 self.has_patchset = False
656
binji@chromium.org0281f522012-09-14 13:37:59 +0000657 def GetMostRecentPatchset(self, issue):
658 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000659 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000660
661 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000662 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000663 '/download/issue%s_%s.diff' % (issue, patchset))
664
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000665 def GetApprovingReviewers(self, issue):
666 return get_approving_reviewers(
667 self.RpcServer().get_issue_properties(int(issue), True))
668
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 def SetIssue(self, issue):
670 """Set this branch's issue. If issue=0, clears the issue."""
671 if issue:
672 RunGit(['config', self._IssueSetting(), str(issue)])
673 if self.rietveld_server:
674 RunGit(['config', self._RietveldServer(), self.rietveld_server])
675 else:
676 RunGit(['config', '--unset', self._IssueSetting()])
677 self.SetPatchset(0)
678 self.has_issue = False
679
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000680 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000681 if not self.GitSanityChecks(upstream_branch):
682 DieWithError('\nGit sanity check failure')
683
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000684 root = RunCommand(['git', '--no-pager', 'rev-parse', '--show-cdup']).strip()
685 if not root:
686 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000687 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000688
689 # We use the sha1 of HEAD as a name of this change.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000690 name = RunCommand(['git', '--no-pager', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000691 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000692 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000693 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000694 except subprocess2.CalledProcessError:
695 DieWithError(
696 ('\nFailed to diff against upstream branch %s!\n\n'
697 'This branch probably doesn\'t exist anymore. To reset the\n'
698 'tracking branch, please run\n'
699 ' git branch --set-upstream %s trunk\n'
700 'replacing trunk with origin/master or the relevant branch') %
701 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000702
maruel@chromium.org52424302012-08-29 15:14:30 +0000703 issue = self.GetIssue()
704 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000705 if issue:
706 description = self.GetDescription()
707 else:
708 # If the change was never uploaded, use the log messages of all commits
709 # up to the branch point, as git cl upload will prefill the description
710 # with these log messages.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000711 description = RunCommand(['git', '--no-pager',
712 'log', '--pretty=format:%s%n%n%b',
maruel@chromium.org373af802012-05-25 21:07:33 +0000713 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000714
715 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000716 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000717 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000718 name,
719 description,
720 absroot,
721 files,
722 issue,
723 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000724 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000725
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000726 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000727 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000728
729 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000730 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000731 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000732 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000733 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000734 except presubmit_support.PresubmitFailure, e:
735 DieWithError(
736 ('%s\nMaybe your depot_tools is out of date?\n'
737 'If all fails, contact maruel@') % e)
738
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000739 def UpdateDescription(self, description):
740 self.description = description
741 return self.RpcServer().update_description(
742 self.GetIssue(), self.description)
743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000745 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000746 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000748 def SetFlag(self, flag, value):
749 """Patchset must match."""
750 if not self.GetPatchset():
751 DieWithError('The patchset needs to match. Send another patchset.')
752 try:
753 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000754 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000755 except urllib2.HTTPError, e:
756 if e.code == 404:
757 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
758 if e.code == 403:
759 DieWithError(
760 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
761 'match?') % (self.GetIssue(), self.GetPatchset()))
762 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000764 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 """Returns an upload.RpcServer() to access this review's rietveld instance.
766 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000767 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000768 self._rpc_server = rietveld.CachingRietveld(
769 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000770 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771
772 def _IssueSetting(self):
773 """Return the git setting that stores this change's issue."""
774 return 'branch.%s.rietveldissue' % self.GetBranch()
775
776 def _PatchsetSetting(self):
777 """Return the git setting that stores this change's most recent patchset."""
778 return 'branch.%s.rietveldpatchset' % self.GetBranch()
779
780 def _RietveldServer(self):
781 """Returns the git setting that stores this change's rietveld server."""
782 return 'branch.%s.rietveldserver' % self.GetBranch()
783
784
785def GetCodereviewSettingsInteractively():
786 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000787 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 server = settings.GetDefaultServerUrl(error_ok=True)
789 prompt = 'Rietveld server (host[:port])'
790 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000791 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 if not server and not newserver:
793 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000794 if newserver:
795 newserver = gclient_utils.UpgradeToHttps(newserver)
796 if newserver != server:
797 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000799 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 prompt = caption
801 if initial:
802 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000803 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000804 if new_val == 'x':
805 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000806 elif new_val:
807 if is_url:
808 new_val = gclient_utils.UpgradeToHttps(new_val)
809 if new_val != initial:
810 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000812 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000813 SetProperty(settings.GetDefaultPrivateFlag(),
814 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000816 'tree-status-url', False)
817 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818
819 # TODO: configure a default branch to diff against, rather than this
820 # svn-based hackery.
821
822
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000823class ChangeDescription(object):
824 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000825 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000826
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000827 def __init__(self, description):
828 self._description = (description or '').strip()
829
830 @property
831 def description(self):
832 return self._description
833
834 def update_reviewers(self, reviewers):
835 """Rewrites the R=/TBR= line(s) as a single line."""
836 assert isinstance(reviewers, list), reviewers
837 if not reviewers:
838 return
839 regexp = re.compile(self.R_LINE, re.MULTILINE)
840 matches = list(regexp.finditer(self._description))
841 is_tbr = any(m.group(1) == 'TBR' for m in matches)
842 if len(matches) > 1:
843 # Erase all except the first one.
844 for i in xrange(len(matches) - 1, 0, -1):
845 self._description = (
846 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000847 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000848
849 if is_tbr:
850 new_r_line = 'TBR=' + ', '.join(reviewers)
851 else:
852 new_r_line = 'R=' + ', '.join(reviewers)
853
854 if matches:
855 self._description = (
856 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000857 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000858 else:
859 self.append_footer(new_r_line)
860
861 def prompt(self):
862 """Asks the user to update the description."""
863 self._description = (
864 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000865 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000866 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000867 '#--------------------This line is 72 characters long'
868 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000869 ) + self._description
870
871 if '\nBUG=' not in self._description:
872 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000873 content = gclient_utils.RunEditor(self._description, True,
874 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000875 if not content:
876 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000877
878 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000879 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000881 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000883
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000884 def append_footer(self, line):
885 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
886 if self._description:
887 if '\n' not in self._description:
888 self._description += '\n'
889 else:
890 last_line = self._description.rsplit('\n', 1)[1]
891 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
892 not presubmit_support.Change.TAG_LINE_RE.match(line)):
893 self._description += '\n'
894 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000895
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000896 def get_reviewers(self):
897 """Retrieves the list of reviewers."""
898 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000899 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000900 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000901
902
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000903def get_approving_reviewers(props):
904 """Retrieves the reviewers that approved a CL from the issue properties with
905 messages.
906
907 Note that the list may contain reviewers that are not committer, thus are not
908 considered by the CQ.
909 """
910 return sorted(
911 set(
912 message['sender']
913 for message in props['messages']
914 if message['approval'] and message['sender'] in props['reviewers']
915 )
916 )
917
918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919def FindCodereviewSettingsFile(filename='codereview.settings'):
920 """Finds the given file starting in the cwd and going up.
921
922 Only looks up to the top of the repository unless an
923 'inherit-review-settings-ok' file exists in the root of the repository.
924 """
925 inherit_ok_file = 'inherit-review-settings-ok'
926 cwd = os.getcwd()
927 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
928 if os.path.isfile(os.path.join(root, inherit_ok_file)):
929 root = '/'
930 while True:
931 if filename in os.listdir(cwd):
932 if os.path.isfile(os.path.join(cwd, filename)):
933 return open(os.path.join(cwd, filename))
934 if cwd == root:
935 break
936 cwd = os.path.dirname(cwd)
937
938
939def LoadCodereviewSettingsFromFile(fileobj):
940 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000941 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943 def SetProperty(name, setting, unset_error_ok=False):
944 fullname = 'rietveld.' + name
945 if setting in keyvals:
946 RunGit(['config', fullname, keyvals[setting]])
947 else:
948 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
949
950 SetProperty('server', 'CODE_REVIEW_SERVER')
951 # Only server setting is required. Other settings can be absent.
952 # In that case, we ignore errors raised during option deletion attempt.
953 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000954 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
956 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
957
ukai@chromium.orge8077812012-02-03 03:41:46 +0000958 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
959 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
960 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
963 #should be of the form
964 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
965 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
966 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
967 keyvals['ORIGIN_URL_CONFIG']])
968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000970def urlretrieve(source, destination):
971 """urllib is broken for SSL connections via a proxy therefore we
972 can't use urllib.urlretrieve()."""
973 with open(destination, 'w') as f:
974 f.write(urllib2.urlopen(source).read())
975
976
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000977def DownloadHooks(force):
978 """downloads hooks
979
980 Args:
981 force: True to update hooks. False to install hooks if not present.
982 """
983 if not settings.GetIsGerrit():
984 return
985 server_url = settings.GetDefaultServerUrl()
986 src = '%s/tools/hooks/commit-msg' % server_url
987 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
988 if not os.access(dst, os.X_OK):
989 if os.path.exists(dst):
990 if not force:
991 return
992 os.remove(dst)
993 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000994 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000995 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
996 except Exception:
997 if os.path.exists(dst):
998 os.remove(dst)
999 DieWithError('\nFailed to download hooks from %s' % src)
1000
1001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002@usage('[repo root containing codereview.settings]')
1003def CMDconfig(parser, args):
1004 """edit configuration for this tree"""
1005
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001006 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007 if len(args) == 0:
1008 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001009 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 return 0
1011
1012 url = args[0]
1013 if not url.endswith('codereview.settings'):
1014 url = os.path.join(url, 'codereview.settings')
1015
1016 # Load code review settings and download hooks (if available).
1017 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001018 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019 return 0
1020
1021
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001022def CMDbaseurl(parser, args):
1023 """get or set base-url for this branch"""
1024 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1025 branch = ShortBranchName(branchref)
1026 _, args = parser.parse_args(args)
1027 if not args:
1028 print("Current base-url:")
1029 return RunGit(['config', 'branch.%s.base-url' % branch],
1030 error_ok=False).strip()
1031 else:
1032 print("Setting base-url to %s" % args[0])
1033 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1034 error_ok=False).strip()
1035
1036
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037def CMDstatus(parser, args):
1038 """show status of changelists"""
1039 parser.add_option('--field',
1040 help='print only specific field (desc|id|patch|url)')
1041 (options, args) = parser.parse_args(args)
1042
1043 # TODO: maybe make show_branches a flag if necessary.
1044 show_branches = not options.field
1045
1046 if show_branches:
1047 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1048 if branches:
1049 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001050 changes = (Changelist(branchref=b) for b in branches.splitlines())
1051 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1052 alignment = max(5, max(len(b) for b in branches))
1053 for branch in sorted(branches):
1054 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055
1056 cl = Changelist()
1057 if options.field:
1058 if options.field.startswith('desc'):
1059 print cl.GetDescription()
1060 elif options.field == 'id':
1061 issueid = cl.GetIssue()
1062 if issueid:
1063 print issueid
1064 elif options.field == 'patch':
1065 patchset = cl.GetPatchset()
1066 if patchset:
1067 print patchset
1068 elif options.field == 'url':
1069 url = cl.GetIssueURL()
1070 if url:
1071 print url
1072 else:
1073 print
1074 print 'Current branch:',
1075 if not cl.GetIssue():
1076 print 'no issue assigned.'
1077 return 0
1078 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001079 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 print 'Issue description:'
1081 print cl.GetDescription(pretty=True)
1082 return 0
1083
1084
1085@usage('[issue_number]')
1086def CMDissue(parser, args):
1087 """Set or display the current code review issue number.
1088
1089 Pass issue number 0 to clear the current issue.
1090"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001091 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092
1093 cl = Changelist()
1094 if len(args) > 0:
1095 try:
1096 issue = int(args[0])
1097 except ValueError:
1098 DieWithError('Pass a number to set the issue or none to list it.\n'
1099 'Maybe you want to run git cl status?')
1100 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001101 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102 return 0
1103
1104
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001105def CMDcomments(parser, args):
1106 """show review comments of the current changelist"""
1107 (_, args) = parser.parse_args(args)
1108 if args:
1109 parser.error('Unsupported argument: %s' % args)
1110
1111 cl = Changelist()
1112 if cl.GetIssue():
1113 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1114 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001115 if message['disapproval']:
1116 color = Fore.RED
1117 elif message['approval']:
1118 color = Fore.GREEN
1119 elif message['sender'] == data['owner_email']:
1120 color = Fore.MAGENTA
1121 else:
1122 color = Fore.BLUE
1123 print '\n%s%s %s%s' % (
1124 color, message['date'].split('.', 1)[0], message['sender'],
1125 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001126 if message['text'].strip():
1127 print '\n'.join(' ' + l for l in message['text'].splitlines())
1128 return 0
1129
1130
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001131def CMDdescription(parser, args):
1132 """brings up the editor for the current CL's description."""
1133 cl = Changelist()
1134 if not cl.GetIssue():
1135 DieWithError('This branch has no associated changelist.')
1136 description = ChangeDescription(cl.GetDescription())
1137 description.prompt()
1138 cl.UpdateDescription(description.description)
1139 return 0
1140
1141
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142def CreateDescriptionFromLog(args):
1143 """Pulls out the commit log to use as a base for the CL description."""
1144 log_args = []
1145 if len(args) == 1 and not args[0].endswith('.'):
1146 log_args = [args[0] + '..']
1147 elif len(args) == 1 and args[0].endswith('...'):
1148 log_args = [args[0][:-1]]
1149 elif len(args) == 2:
1150 log_args = [args[0] + '..' + args[1]]
1151 else:
1152 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001153 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154
1155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156def CMDpresubmit(parser, args):
1157 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001158 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001160 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001161 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 (options, args) = parser.parse_args(args)
1163
ukai@chromium.org259e4682012-10-25 07:36:33 +00001164 if not options.force and is_dirty_git_tree('presubmit'):
1165 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 return 1
1167
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001168 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 if args:
1170 base_branch = args[0]
1171 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001172 # Default to diffing against the common ancestor of the upstream branch.
1173 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001175 cl.RunHook(
1176 committing=not options.upload,
1177 may_prompt=False,
1178 verbose=options.verbose,
1179 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001180 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181
1182
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001183def AddChangeIdToCommitMessage(options, args):
1184 """Re-commits using the current message, assumes the commit hook is in
1185 place.
1186 """
1187 log_desc = options.message or CreateDescriptionFromLog(args)
1188 git_command = ['commit', '--amend', '-m', log_desc]
1189 RunGit(git_command)
1190 new_log_desc = CreateDescriptionFromLog(args)
1191 if CHANGE_ID in new_log_desc:
1192 print 'git-cl: Added Change-Id to commit message.'
1193 else:
1194 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1195
1196
ukai@chromium.orge8077812012-02-03 03:41:46 +00001197def GerritUpload(options, args, cl):
1198 """upload the current branch to gerrit."""
1199 # We assume the remote called "origin" is the one we want.
1200 # It is probably not worthwhile to support different workflows.
1201 remote = 'origin'
1202 branch = 'master'
1203 if options.target_branch:
1204 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001206 change_desc = ChangeDescription(
1207 options.message or CreateDescriptionFromLog(args))
1208 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001209 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001211 if CHANGE_ID not in change_desc.description:
1212 AddChangeIdToCommitMessage(options, args)
1213 if options.reviewers:
1214 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
ukai@chromium.orge8077812012-02-03 03:41:46 +00001216 receive_options = []
1217 cc = cl.GetCCList().split(',')
1218 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001219 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001220 cc = filter(None, cc)
1221 if cc:
1222 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001223 if change_desc.get_reviewers():
1224 receive_options.extend(
1225 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226
ukai@chromium.orge8077812012-02-03 03:41:46 +00001227 git_command = ['push']
1228 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001229 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001230 ' '.join(receive_options))
1231 git_command += [remote, 'HEAD:refs/for/' + branch]
1232 RunGit(git_command)
1233 # TODO(ukai): parse Change-Id: and set issue number?
1234 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001235
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236
ukai@chromium.orge8077812012-02-03 03:41:46 +00001237def RietveldUpload(options, args, cl):
1238 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1240 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 if options.emulate_svn_auto_props:
1242 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243
1244 change_desc = None
1245
1246 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001247 if options.title:
1248 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001249 if options.message:
1250 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001251 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 print ("This branch is associated with issue %s. "
1253 "Adding patch to that issue." % cl.GetIssue())
1254 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001255 if options.title:
1256 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001257 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001258 change_desc = ChangeDescription(message)
1259 if options.reviewers:
1260 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001261 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001262 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001263
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001264 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 print "Description is empty; aborting."
1266 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001267
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001268 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001269 if change_desc.get_reviewers():
1270 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001271 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001272 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001273 DieWithError("Must specify reviewers to send email.")
1274 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001275 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001276 if cc:
1277 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001279 if options.private or settings.GetDefaultPrivateFlag() == "True":
1280 upload_args.append('--private')
1281
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001282 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001283 if not options.find_copies:
1284 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 # Include the upstream repo's URL in the change -- this is useful for
1287 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001288 remote_url = cl.GetGitBaseUrlFromConfig()
1289 if not remote_url:
1290 if settings.GetIsGitSvn():
1291 # URL is dependent on the current directory.
1292 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1293 if data:
1294 keys = dict(line.split(': ', 1) for line in data.splitlines()
1295 if ': ' in line)
1296 remote_url = keys.get('URL', None)
1297 else:
1298 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1299 remote_url = (cl.GetRemoteUrl() + '@'
1300 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 if remote_url:
1302 upload_args.extend(['--base_url', remote_url])
1303
1304 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001305 upload_args = ['upload'] + upload_args + args
1306 logging.info('upload.RealMain(%s)', upload_args)
1307 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001308 except KeyboardInterrupt:
1309 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 except:
1311 # If we got an exception after the user typed a description for their
1312 # change, back up the description before re-raising.
1313 if change_desc:
1314 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1315 print '\nGot exception while uploading -- saving description to %s\n' \
1316 % backup_path
1317 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001318 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319 backup_file.close()
1320 raise
1321
1322 if not cl.GetIssue():
1323 cl.SetIssue(issue)
1324 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001325
1326 if options.use_commit_queue:
1327 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 return 0
1329
1330
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001331def cleanup_list(l):
1332 """Fixes a list so that comma separated items are put as individual items.
1333
1334 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1335 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1336 """
1337 items = sum((i.split(',') for i in l), [])
1338 stripped_items = (i.strip() for i in items)
1339 return sorted(filter(None, stripped_items))
1340
1341
ukai@chromium.orge8077812012-02-03 03:41:46 +00001342@usage('[args to "git diff"]')
1343def CMDupload(parser, args):
1344 """upload the current changelist to codereview"""
1345 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1346 help='bypass upload presubmit hook')
1347 parser.add_option('-f', action='store_true', dest='force',
1348 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001349 parser.add_option('-m', dest='message', help='message for patchset')
1350 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001351 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001352 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001353 help='reviewer email addresses')
1354 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001355 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001356 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001357 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001358 help='send email to reviewer immediately')
1359 parser.add_option("--emulate_svn_auto_props", action="store_true",
1360 dest="emulate_svn_auto_props",
1361 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001362 parser.add_option('-c', '--use-commit-queue', action='store_true',
1363 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001364 parser.add_option('--private', action='store_true',
1365 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001366 parser.add_option('--target_branch',
1367 help='When uploading to gerrit, remote branch to '
1368 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001369 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001370 (options, args) = parser.parse_args(args)
1371
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001372 if options.target_branch and not settings.GetIsGerrit():
1373 parser.error('Use --target_branch for non gerrit repository.')
1374
ukai@chromium.org259e4682012-10-25 07:36:33 +00001375 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001376 return 1
1377
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001378 options.reviewers = cleanup_list(options.reviewers)
1379 options.cc = cleanup_list(options.cc)
1380
ukai@chromium.orge8077812012-02-03 03:41:46 +00001381 cl = Changelist()
1382 if args:
1383 # TODO(ukai): is it ok for gerrit case?
1384 base_branch = args[0]
1385 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001386 # Default to diffing against common ancestor of upstream branch
1387 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001388 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001389
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001390 # Apply watchlists on upload.
1391 change = cl.GetChange(base_branch, None)
1392 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1393 files = [f.LocalPath() for f in change.AffectedFiles()]
1394 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1395
ukai@chromium.orge8077812012-02-03 03:41:46 +00001396 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001397 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001398 may_prompt=not options.force,
1399 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001400 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001401 if not hook_results.should_continue():
1402 return 1
1403 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001404 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001405
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001406 if cl.GetIssue():
1407 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1408 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001409 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001410 print ('The last upload made from this repository was patchset #%d but '
1411 'the most recent patchset on the server is #%d.'
1412 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001413 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1414 'from another machine or branch the patch you\'re uploading now '
1415 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001416 ask_for_data('About to upload; enter to confirm.')
1417
iannucci@chromium.org79540052012-10-19 23:15:26 +00001418 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001419 if settings.GetIsGerrit():
1420 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001421 ret = RietveldUpload(options, args, cl)
1422 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001423 git_set_branch_value('last-upload-hash',
1424 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001425
1426 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001427
1428
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001429def IsSubmoduleMergeCommit(ref):
1430 # When submodules are added to the repo, we expect there to be a single
1431 # non-git-svn merge commit at remote HEAD with a signature comment.
1432 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001433 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001434 return RunGit(cmd) != ''
1435
1436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437def SendUpstream(parser, args, cmd):
1438 """Common code for CmdPush and CmdDCommit
1439
1440 Squashed commit into a single.
1441 Updates changelog with metadata (e.g. pointer to review).
1442 Pushes/dcommits the code upstream.
1443 Updates review and closes.
1444 """
1445 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1446 help='bypass upload presubmit hook')
1447 parser.add_option('-m', dest='message',
1448 help="override review description")
1449 parser.add_option('-f', action='store_true', dest='force',
1450 help="force yes to questions (don't prompt)")
1451 parser.add_option('-c', dest='contributor',
1452 help="external contributor for patch (appended to " +
1453 "description and used as author for git). Should be " +
1454 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001455 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 (options, args) = parser.parse_args(args)
1457 cl = Changelist()
1458
1459 if not args or cmd == 'push':
1460 # Default to merging against our best guess of the upstream branch.
1461 args = [cl.GetUpstreamBranch()]
1462
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001463 if options.contributor:
1464 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1465 print "Please provide contibutor as 'First Last <email@example.com>'"
1466 return 1
1467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001469 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470
ukai@chromium.org259e4682012-10-25 07:36:33 +00001471 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 return 1
1473
1474 # This rev-list syntax means "show all commits not in my branch that
1475 # are in base_branch".
1476 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1477 base_branch]).splitlines()
1478 if upstream_commits:
1479 print ('Base branch "%s" has %d commits '
1480 'not in this branch.' % (base_branch, len(upstream_commits)))
1481 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1482 return 1
1483
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001484 # This is the revision `svn dcommit` will commit on top of.
1485 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1486 '--pretty=format:%H'])
1487
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001489 # If the base_head is a submodule merge commit, the first parent of the
1490 # base_head should be a git-svn commit, which is what we're interested in.
1491 base_svn_head = base_branch
1492 if base_has_submodules:
1493 base_svn_head += '^1'
1494
1495 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496 if extra_commits:
1497 print ('This branch has %d additional commits not upstreamed yet.'
1498 % len(extra_commits.splitlines()))
1499 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1500 'before attempting to %s.' % (base_branch, cmd))
1501 return 1
1502
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001503 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001504 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001505 author = None
1506 if options.contributor:
1507 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001508 hook_results = cl.RunHook(
1509 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001510 may_prompt=not options.force,
1511 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001512 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001513 if not hook_results.should_continue():
1514 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515
1516 if cmd == 'dcommit':
1517 # Check the tree status if the tree status URL is set.
1518 status = GetTreeStatus()
1519 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001520 print('The tree is closed. Please wait for it to reopen. Use '
1521 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001522 return 1
1523 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001524 print('Unable to determine tree status. Please verify manually and '
1525 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001526 else:
1527 breakpad.SendStack(
1528 'GitClHooksBypassedCommit',
1529 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001530 (cl.GetRietveldServer(), cl.GetIssue()),
1531 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001533 change_desc = ChangeDescription(options.message)
1534 if not change_desc.description and cl.GetIssue():
1535 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001538 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001539 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001540 else:
1541 print 'No description set.'
1542 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1543 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001545 # Keep a separate copy for the commit message, because the commit message
1546 # contains the link to the Rietveld issue, while the Rietveld message contains
1547 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001548 # Keep a separate copy for the commit message.
1549 if cl.GetIssue():
1550 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1551
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001552 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001553 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001554 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001555 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001556 commit_desc.append_footer('Patch from %s.' % options.contributor)
1557
1558 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559
1560 branches = [base_branch, cl.GetBranchRef()]
1561 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001562 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001563 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001565 # We want to squash all this branch's commits into one commit with the proper
1566 # description. We do this by doing a "reset --soft" to the base branch (which
1567 # keeps the working copy the same), then dcommitting that. If origin/master
1568 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1569 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001570 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001571 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1572 # Delete the branches if they exist.
1573 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1574 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1575 result = RunGitWithCode(showref_cmd)
1576 if result[0] == 0:
1577 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578
1579 # We might be in a directory that's present in this branch but not in the
1580 # trunk. Move up to the top of the tree so that git commands that expect a
1581 # valid CWD won't fail after we check out the merge branch.
1582 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1583 if rel_base_path:
1584 os.chdir(rel_base_path)
1585
1586 # Stuff our change into the merge branch.
1587 # We wrap in a try...finally block so if anything goes wrong,
1588 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001589 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001590 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001591 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1592 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001594 RunGit(
1595 [
1596 'commit', '--author', options.contributor,
1597 '-m', commit_desc.description,
1598 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001599 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001600 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001601 if base_has_submodules:
1602 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1603 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1604 RunGit(['checkout', CHERRY_PICK_BRANCH])
1605 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001606 if cmd == 'push':
1607 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001608 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 retcode, output = RunGitWithCode(
1610 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1611 logging.debug(output)
1612 else:
1613 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001614 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001615 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001616 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001617 finally:
1618 # And then swap back to the original branch and clean up.
1619 RunGit(['checkout', '-q', cl.GetBranch()])
1620 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001621 if base_has_submodules:
1622 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623
1624 if cl.GetIssue():
1625 if cmd == 'dcommit' and 'Committed r' in output:
1626 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1627 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001628 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1629 for l in output.splitlines(False))
1630 match = filter(None, match)
1631 if len(match) != 1:
1632 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1633 output)
1634 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001635 else:
1636 return 1
1637 viewvc_url = settings.GetViewVCUrl()
1638 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001639 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001640 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001641 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 print ('Closing issue '
1643 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001644 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001646 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001647 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001648 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001649 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1650 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001652
1653 if retcode == 0:
1654 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1655 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001656 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001657
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001658 return 0
1659
1660
1661@usage('[upstream branch to apply against]')
1662def CMDdcommit(parser, args):
1663 """commit the current changelist via git-svn"""
1664 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001665 message = """This doesn't appear to be an SVN repository.
1666If your project has a git mirror with an upstream SVN master, you probably need
1667to run 'git svn init', see your project's git mirror documentation.
1668If your project has a true writeable upstream repository, you probably want
1669to run 'git cl push' instead.
1670Choose wisely, if you get this wrong, your commit might appear to succeed but
1671will instead be silently ignored."""
1672 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001673 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674 return SendUpstream(parser, args, 'dcommit')
1675
1676
1677@usage('[upstream branch to apply against]')
1678def CMDpush(parser, args):
1679 """commit the current changelist via git"""
1680 if settings.GetIsGitSvn():
1681 print('This appears to be an SVN repository.')
1682 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001683 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001684 return SendUpstream(parser, args, 'push')
1685
1686
1687@usage('<patch url or issue id>')
1688def CMDpatch(parser, args):
1689 """patch in a code review"""
1690 parser.add_option('-b', dest='newbranch',
1691 help='create a new branch off trunk for the patch')
1692 parser.add_option('-f', action='store_true', dest='force',
1693 help='with -b, clobber any existing branch')
1694 parser.add_option('--reject', action='store_true', dest='reject',
1695 help='allow failed patches and spew .rej files')
1696 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1697 help="don't commit after patch applies")
1698 (options, args) = parser.parse_args(args)
1699 if len(args) != 1:
1700 parser.print_help()
1701 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001702 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001703
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001704 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001705 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001706
maruel@chromium.org52424302012-08-29 15:14:30 +00001707 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001709 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001710 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001711 patchset = cl.GetMostRecentPatchset(issue)
1712 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001714 # Assume it's a URL to the patch. Default to https.
1715 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001716 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001717 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718 DieWithError('Must pass an issue ID or full URL for '
1719 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001720 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001721 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001722 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723
1724 if options.newbranch:
1725 if options.force:
1726 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001727 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728 RunGit(['checkout', '-b', options.newbranch,
1729 Changelist().GetUpstreamBranch()])
1730
1731 # Switch up to the top-level directory, if necessary, in preparation for
1732 # applying the patch.
1733 top = RunGit(['rev-parse', '--show-cdup']).strip()
1734 if top:
1735 os.chdir(top)
1736
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737 # Git patches have a/ at the beginning of source paths. We strip that out
1738 # with a sed script rather than the -p flag to patch so we can feed either
1739 # Git or svn-style patches into the same apply command.
1740 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001741 try:
1742 patch_data = subprocess2.check_output(
1743 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1744 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745 DieWithError('Git patch mungling failed.')
1746 logging.info(patch_data)
1747 # We use "git apply" to apply the patch instead of "patch" so that we can
1748 # pick up file adds.
1749 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001750 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751 if options.reject:
1752 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001753 try:
1754 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1755 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 DieWithError('Failed to apply the patch')
1757
1758 # If we had an issue, commit the current state and register the issue.
1759 if not options.nocommit:
1760 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1761 cl = Changelist()
1762 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001763 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001764 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 else:
1766 print "Patch applied to index."
1767 return 0
1768
1769
1770def CMDrebase(parser, args):
1771 """rebase current branch on top of svn repo"""
1772 # Provide a wrapper for git svn rebase to help avoid accidental
1773 # git svn dcommit.
1774 # It's the only command that doesn't use parser at all since we just defer
1775 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001776 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777
1778
1779def GetTreeStatus():
1780 """Fetches the tree status and returns either 'open', 'closed',
1781 'unknown' or 'unset'."""
1782 url = settings.GetTreeStatusUrl(error_ok=True)
1783 if url:
1784 status = urllib2.urlopen(url).read().lower()
1785 if status.find('closed') != -1 or status == '0':
1786 return 'closed'
1787 elif status.find('open') != -1 or status == '1':
1788 return 'open'
1789 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790 return 'unset'
1791
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001793def GetTreeStatusReason():
1794 """Fetches the tree status from a json url and returns the message
1795 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001796 url = settings.GetTreeStatusUrl()
1797 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001798 connection = urllib2.urlopen(json_url)
1799 status = json.loads(connection.read())
1800 connection.close()
1801 return status['message']
1802
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001803
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001804def CMDtree(parser, args):
1805 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001806 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001807 status = GetTreeStatus()
1808 if 'unset' == status:
1809 print 'You must configure your tree status URL by running "git cl config".'
1810 return 2
1811
1812 print "The tree is %s" % status
1813 print
1814 print GetTreeStatusReason()
1815 if status != 'open':
1816 return 1
1817 return 0
1818
1819
maruel@chromium.org15192402012-09-06 12:38:29 +00001820def CMDtry(parser, args):
1821 """Triggers a try job through Rietveld."""
1822 group = optparse.OptionGroup(parser, "Try job options")
1823 group.add_option(
1824 "-b", "--bot", action="append",
1825 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1826 "times to specify multiple builders. ex: "
1827 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1828 "the try server waterfall for the builders name and the tests "
1829 "available. Can also be used to specify gtest_filter, e.g. "
1830 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1831 group.add_option(
1832 "-r", "--revision",
1833 help="Revision to use for the try job; default: the "
1834 "revision will be determined by the try server; see "
1835 "its waterfall for more info")
1836 group.add_option(
1837 "-c", "--clobber", action="store_true", default=False,
1838 help="Force a clobber before building; e.g. don't do an "
1839 "incremental build")
1840 group.add_option(
1841 "--project",
1842 help="Override which project to use. Projects are defined "
1843 "server-side to define what default bot set to use")
1844 group.add_option(
1845 "-t", "--testfilter", action="append", default=[],
1846 help=("Apply a testfilter to all the selected builders. Unless the "
1847 "builders configurations are similar, use multiple "
1848 "--bot <builder>:<test> arguments."))
1849 group.add_option(
1850 "-n", "--name", help="Try job name; default to current branch name")
1851 parser.add_option_group(group)
1852 options, args = parser.parse_args(args)
1853
1854 if args:
1855 parser.error('Unknown arguments: %s' % args)
1856
1857 cl = Changelist()
1858 if not cl.GetIssue():
1859 parser.error('Need to upload first')
1860
1861 if not options.name:
1862 options.name = cl.GetBranch()
1863
1864 # Process --bot and --testfilter.
1865 if not options.bot:
1866 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001867 change = cl.GetChange(
1868 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1869 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001870 options.bot = presubmit_support.DoGetTrySlaves(
1871 change,
1872 change.LocalPaths(),
1873 settings.GetRoot(),
1874 None,
1875 None,
1876 options.verbose,
1877 sys.stdout)
1878 if not options.bot:
1879 parser.error('No default try builder to try, use --bot')
1880
1881 builders_and_tests = {}
1882 for bot in options.bot:
1883 if ':' in bot:
1884 builder, tests = bot.split(':', 1)
1885 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1886 elif ',' in bot:
1887 parser.error('Specify one bot per --bot flag')
1888 else:
1889 builders_and_tests.setdefault(bot, []).append('defaulttests')
1890
1891 if options.testfilter:
1892 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1893 builders_and_tests = dict(
1894 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1895 if t != ['compile'])
1896
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001897 if any('triggered' in b for b in builders_and_tests):
1898 print >> sys.stderr, (
1899 'ERROR You are trying to send a job to a triggered bot. This type of'
1900 ' bot requires an\ninitial job from a parent (usually a builder). '
1901 'Instead send your job to the parent.\n'
1902 'Bot list: %s' % builders_and_tests)
1903 return 1
1904
maruel@chromium.org15192402012-09-06 12:38:29 +00001905 patchset = cl.GetPatchset()
1906 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001907 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001908
1909 cl.RpcServer().trigger_try_jobs(
1910 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1911 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001912 print('Tried jobs on:')
1913 length = max(len(builder) for builder in builders_and_tests)
1914 for builder in sorted(builders_and_tests):
1915 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001916 return 0
1917
1918
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001919@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001921 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001922 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001923 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001924 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001925 return 0
1926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001928 if args:
1929 # One arg means set upstream branch.
1930 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1931 cl = Changelist()
1932 print "Upstream branch set to " + cl.GetUpstreamBranch()
1933 else:
1934 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001935 return 0
1936
1937
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001938def CMDset_commit(parser, args):
1939 """set the commit bit"""
1940 _, args = parser.parse_args(args)
1941 if args:
1942 parser.error('Unrecognized args: %s' % ' '.join(args))
1943 cl = Changelist()
1944 cl.SetFlag('commit', '1')
1945 return 0
1946
1947
groby@chromium.org411034a2013-02-26 15:12:01 +00001948def CMDset_close(parser, args):
1949 """close the issue"""
1950 _, args = parser.parse_args(args)
1951 if args:
1952 parser.error('Unrecognized args: %s' % ' '.join(args))
1953 cl = Changelist()
1954 # Ensure there actually is an issue to close.
1955 cl.GetDescription()
1956 cl.CloseIssue()
1957 return 0
1958
1959
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001960def CMDformat(parser, args):
1961 """run clang-format on the diff"""
1962 CLANG_EXTS = ['.cc', '.cpp', '.h']
1963 parser.add_option('--full', action='store_true', default=False)
1964 opts, args = parser.parse_args(args)
1965 if args:
1966 parser.error('Unrecognized args: %s' % ' '.join(args))
1967
digit@chromium.org29e47272013-05-17 17:01:46 +00001968 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00001969 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001970 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00001971 # Only list the names of modified files.
1972 diff_cmd.append('--name-only')
1973 else:
1974 # Only generate context-less patches.
1975 diff_cmd.append('-U0')
1976
1977 # Grab the merge-base commit, i.e. the upstream commit of the current
1978 # branch when it was created or the last time it was rebased. This is
1979 # to cover the case where the user may have called "git fetch origin",
1980 # moving the origin branch to a newer commit, but hasn't rebased yet.
1981 upstream_commit = None
1982 cl = Changelist()
1983 upstream_branch = cl.GetUpstreamBranch()
1984 if upstream_branch:
1985 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
1986 upstream_commit = upstream_commit.strip()
1987
1988 if not upstream_commit:
1989 DieWithError('Could not find base commit for this branch. '
1990 'Are you in detached state?')
1991
1992 diff_cmd.append(upstream_commit)
1993
1994 # Handle source file filtering.
1995 diff_cmd.append('--')
1996 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
1997 diff_output = RunGit(diff_cmd)
1998
1999 if opts.full:
2000 # diff_output is a list of files to send to clang-format.
2001 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002002 if not files:
2003 print "Nothing to format."
2004 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002005 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002006 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002007 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002008 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2009 'clang-format-diff.py')
2010 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002011 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2012 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2013 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002014
2015 return 0
2016
2017
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018def Command(name):
2019 return getattr(sys.modules[__name__], 'CMD' + name, None)
2020
2021
2022def CMDhelp(parser, args):
2023 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002024 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002025 if len(args) == 1:
2026 return main(args + ['--help'])
2027 parser.print_help()
2028 return 0
2029
2030
2031def GenUsage(parser, command):
2032 """Modify an OptParse object with the function's documentation."""
2033 obj = Command(command)
2034 more = getattr(obj, 'usage_more', '')
2035 if command == 'help':
2036 command = '<command>'
2037 else:
2038 # OptParser.description prefer nicely non-formatted strings.
2039 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2040 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2041
2042
2043def main(argv):
2044 """Doesn't parse the arguments here, just find the right subcommand to
2045 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002046 if sys.hexversion < 0x02060000:
2047 print >> sys.stderr, (
2048 '\nYour python version %s is unsupported, please upgrade.\n' %
2049 sys.version.split(' ', 1)[0])
2050 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002051
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002052 # Reload settings.
2053 global settings
2054 settings = Settings()
2055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056 # Do it late so all commands are listed.
2057 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
2058 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
2059 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
2060
2061 # Create the option parse and add --verbose support.
2062 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002063 parser.add_option(
2064 '-v', '--verbose', action='count', default=0,
2065 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002066 old_parser_args = parser.parse_args
2067 def Parse(args):
2068 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002069 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002071 elif options.verbose:
2072 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002073 else:
2074 logging.basicConfig(level=logging.WARNING)
2075 return options, args
2076 parser.parse_args = Parse
2077
2078 if argv:
2079 command = Command(argv[0])
2080 if command:
2081 # "fix" the usage and the description now that we know the subcommand.
2082 GenUsage(parser, argv[0])
2083 try:
2084 return command(parser, argv[1:])
2085 except urllib2.HTTPError, e:
2086 if e.code != 500:
2087 raise
2088 DieWithError(
2089 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2090 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2091
2092 # Not a known command. Default to help.
2093 GenUsage(parser, 'help')
2094 return CMDhelp(parser, argv)
2095
2096
2097if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002098 # These affect sys.stdout so do it outside of main() to simplify mocks in
2099 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002100 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002101 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102 sys.exit(main(sys.argv[1:]))