blob: f836a0501f82119c3fccae9cab94b3b587efe55e [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
27from third_party import upload
28import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000029import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000032import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000034import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import watchlists
36
37
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000038DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000039POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000041GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000042CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000043
maruel@chromium.org90541732011-04-01 17:54:18 +000044
maruel@chromium.orgddd59412011-11-30 14:20:38 +000045# Initialized in main()
46settings = None
47
48
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000050 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051 sys.exit(1)
52
53
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000056 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057 except subprocess2.CalledProcessError, e:
58 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060 'Command "%s" failed.\n%s' % (
61 ' '.join(args), error_message or e.stdout or ''))
62 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063
64
65def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 """Returns stdout."""
67 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
69
70def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000072 try:
73 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
74 return code, out[0]
75 except ValueError:
76 # When the subprocess fails, it returns None. That triggers a ValueError
77 # when trying to unpack the return value into (out, code).
78 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
80
81def usage(more):
82 def hook(fn):
83 fn.usage_more = more
84 return fn
85 return hook
86
87
maruel@chromium.org90541732011-04-01 17:54:18 +000088def ask_for_data(prompt):
89 try:
90 return raw_input(prompt)
91 except KeyboardInterrupt:
92 # Hide the exception.
93 sys.exit(1)
94
95
iannucci@chromium.org79540052012-10-19 23:15:26 +000096def git_set_branch_value(key, value):
97 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +000098 if not branch:
99 return
100
101 cmd = ['config']
102 if isinstance(value, int):
103 cmd.append('--int')
104 git_key = 'branch.%s.%s' % (branch, key)
105 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000106
107
108def git_get_branch_default(key, default):
109 branch = Changelist().GetBranch()
110 if branch:
111 git_key = 'branch.%s.%s' % (branch, key)
112 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
113 try:
114 return int(stdout.strip())
115 except ValueError:
116 pass
117 return default
118
119
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000120def add_git_similarity(parser):
121 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000122 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000123 help='Sets the percentage that a pair of files need to match in order to'
124 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000125 parser.add_option(
126 '--find-copies', action='store_true',
127 help='Allows git to look for copies.')
128 parser.add_option(
129 '--no-find-copies', action='store_false', dest='find_copies',
130 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000131
132 old_parser_args = parser.parse_args
133 def Parse(args):
134 options, args = old_parser_args(args)
135
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000136 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000137 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000138 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 print('Note: Saving similarity of %d%% in git config.'
140 % options.similarity)
141 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000142
iannucci@chromium.org79540052012-10-19 23:15:26 +0000143 options.similarity = max(0, min(options.similarity, 100))
144
145 if options.find_copies is None:
146 options.find_copies = bool(
147 git_get_branch_default('git-find-copies', True))
148 else:
149 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000150
151 print('Using %d%% similarity for rename/copy detection. '
152 'Override with --similarity.' % options.similarity)
153
154 return options, args
155 parser.parse_args = Parse
156
157
ukai@chromium.org259e4682012-10-25 07:36:33 +0000158def is_dirty_git_tree(cmd):
159 # Make sure index is up-to-date before running diff-index.
160 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
161 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
162 if dirty:
163 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
164 print 'Uncommitted files: (git diff-index --name-status HEAD)'
165 print dirty[:4096]
166 if len(dirty) > 4096:
167 print '... (run "git diff-index --name-status HEAD" to see full output).'
168 return True
169 return False
170
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000171
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000172def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
173 """Return the corresponding git ref if |base_url| together with |glob_spec|
174 matches the full |url|.
175
176 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
177 """
178 fetch_suburl, as_ref = glob_spec.split(':')
179 if allow_wildcards:
180 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
181 if glob_match:
182 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
183 # "branches/{472,597,648}/src:refs/remotes/svn/*".
184 branch_re = re.escape(base_url)
185 if glob_match.group(1):
186 branch_re += '/' + re.escape(glob_match.group(1))
187 wildcard = glob_match.group(2)
188 if wildcard == '*':
189 branch_re += '([^/]*)'
190 else:
191 # Escape and replace surrounding braces with parentheses and commas
192 # with pipe symbols.
193 wildcard = re.escape(wildcard)
194 wildcard = re.sub('^\\\\{', '(', wildcard)
195 wildcard = re.sub('\\\\,', '|', wildcard)
196 wildcard = re.sub('\\\\}$', ')', wildcard)
197 branch_re += wildcard
198 if glob_match.group(3):
199 branch_re += re.escape(glob_match.group(3))
200 match = re.match(branch_re, url)
201 if match:
202 return re.sub('\*$', match.group(1), as_ref)
203
204 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
205 if fetch_suburl:
206 full_url = base_url + '/' + fetch_suburl
207 else:
208 full_url = base_url
209 if full_url == url:
210 return as_ref
211 return None
212
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000213
iannucci@chromium.org79540052012-10-19 23:15:26 +0000214def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000215 """Prints statistics about the change to the user."""
216 # --no-ext-diff is broken in some versions of Git, so try to work around
217 # this by overriding the environment (but there is still a problem if the
218 # git config key "diff.external" is used).
219 env = os.environ.copy()
220 if 'GIT_EXTERNAL_DIFF' in env:
221 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000222
223 if find_copies:
224 similarity_options = ['--find-copies-harder', '-l100000',
225 '-C%s' % similarity]
226 else:
227 similarity_options = ['-M%s' % similarity]
228
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000229 return subprocess2.call(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000230 ['git', 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
231 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000232
233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000234class Settings(object):
235 def __init__(self):
236 self.default_server = None
237 self.cc = None
238 self.root = None
239 self.is_git_svn = None
240 self.svn_branch = None
241 self.tree_status_url = None
242 self.viewvc_url = None
243 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000244 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000245
246 def LazyUpdateIfNeeded(self):
247 """Updates the settings from a codereview.settings file, if available."""
248 if not self.updated:
249 cr_settings_file = FindCodereviewSettingsFile()
250 if cr_settings_file:
251 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000252 self.updated = True
253 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000254 self.updated = True
255
256 def GetDefaultServerUrl(self, error_ok=False):
257 if not self.default_server:
258 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000259 self.default_server = gclient_utils.UpgradeToHttps(
260 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000261 if error_ok:
262 return self.default_server
263 if not self.default_server:
264 error_message = ('Could not find settings file. You must configure '
265 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000266 self.default_server = gclient_utils.UpgradeToHttps(
267 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268 return self.default_server
269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000270 def GetRoot(self):
271 if not self.root:
272 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
273 return self.root
274
275 def GetIsGitSvn(self):
276 """Return true if this repo looks like it's using git-svn."""
277 if self.is_git_svn is None:
278 # If you have any "svn-remote.*" config keys, we think you're using svn.
279 self.is_git_svn = RunGitWithCode(
280 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
281 return self.is_git_svn
282
283 def GetSVNBranch(self):
284 if self.svn_branch is None:
285 if not self.GetIsGitSvn():
286 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
287
288 # Try to figure out which remote branch we're based on.
289 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000290 # 1) iterate through our branch history and find the svn URL.
291 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292
293 # regexp matching the git-svn line that contains the URL.
294 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
295
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000296 # We don't want to go through all of history, so read a line from the
297 # pipe at a time.
298 # The -100 is an arbitrary limit so we don't search forever.
299 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000300 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000301 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000302 for line in proc.stdout:
303 match = git_svn_re.match(line)
304 if match:
305 url = match.group(1)
306 proc.stdout.close() # Cut pipe.
307 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000308
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000309 if url:
310 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
311 remotes = RunGit(['config', '--get-regexp',
312 r'^svn-remote\..*\.url']).splitlines()
313 for remote in remotes:
314 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000315 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000316 remote = match.group(1)
317 base_url = match.group(2)
318 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000319 ['config', 'svn-remote.%s.fetch' % remote],
320 error_ok=True).strip()
321 if fetch_spec:
322 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
323 if self.svn_branch:
324 break
325 branch_spec = RunGit(
326 ['config', 'svn-remote.%s.branches' % remote],
327 error_ok=True).strip()
328 if branch_spec:
329 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
330 if self.svn_branch:
331 break
332 tag_spec = RunGit(
333 ['config', 'svn-remote.%s.tags' % remote],
334 error_ok=True).strip()
335 if tag_spec:
336 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
337 if self.svn_branch:
338 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000339
340 if not self.svn_branch:
341 DieWithError('Can\'t guess svn branch -- try specifying it on the '
342 'command line')
343
344 return self.svn_branch
345
346 def GetTreeStatusUrl(self, error_ok=False):
347 if not self.tree_status_url:
348 error_message = ('You must configure your tree status URL by running '
349 '"git cl config".')
350 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
351 error_ok=error_ok,
352 error_message=error_message)
353 return self.tree_status_url
354
355 def GetViewVCUrl(self):
356 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000357 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000358 return self.viewvc_url
359
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000360 def GetDefaultCCList(self):
361 return self._GetConfig('rietveld.cc', error_ok=True)
362
ukai@chromium.orge8077812012-02-03 03:41:46 +0000363 def GetIsGerrit(self):
364 """Return true if this repo is assosiated with gerrit code review system."""
365 if self.is_gerrit is None:
366 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
367 return self.is_gerrit
368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000369 def _GetConfig(self, param, **kwargs):
370 self.LazyUpdateIfNeeded()
371 return RunGit(['config', param], **kwargs).strip()
372
373
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000374def ShortBranchName(branch):
375 """Convert a name like 'refs/heads/foo' to just 'foo'."""
376 return branch.replace('refs/heads/', '')
377
378
379class Changelist(object):
380 def __init__(self, branchref=None):
381 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000382 global settings
383 if not settings:
384 # Happens when git_cl.py is used as a utility library.
385 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000386 settings.GetDefaultServerUrl()
387 self.branchref = branchref
388 if self.branchref:
389 self.branch = ShortBranchName(self.branchref)
390 else:
391 self.branch = None
392 self.rietveld_server = None
393 self.upstream_branch = None
394 self.has_issue = False
395 self.issue = None
396 self.has_description = False
397 self.description = None
398 self.has_patchset = False
399 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000400 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000401 self.cc = None
402 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000403 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000404
405 def GetCCList(self):
406 """Return the users cc'd on this CL.
407
408 Return is a string suitable for passing to gcl with the --cc flag.
409 """
410 if self.cc is None:
411 base_cc = settings .GetDefaultCCList()
412 more_cc = ','.join(self.watchers)
413 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
414 return self.cc
415
416 def SetWatchers(self, watchers):
417 """Set the list of email addresses that should be cc'd based on the changed
418 files in this CL.
419 """
420 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000421
422 def GetBranch(self):
423 """Returns the short branch name, e.g. 'master'."""
424 if not self.branch:
425 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
426 self.branch = ShortBranchName(self.branchref)
427 return self.branch
428
429 def GetBranchRef(self):
430 """Returns the full branch name, e.g. 'refs/heads/master'."""
431 self.GetBranch() # Poke the lazy loader.
432 return self.branchref
433
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000434 @staticmethod
435 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 """Returns a tuple containg remote and remote ref,
437 e.g. 'origin', 'refs/heads/master'
438 """
439 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
441 error_ok=True).strip()
442 if upstream_branch:
443 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
444 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000445 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
446 error_ok=True).strip()
447 if upstream_branch:
448 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000450 # Fall back on trying a git-svn upstream branch.
451 if settings.GetIsGitSvn():
452 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000454 # Else, try to guess the origin remote.
455 remote_branches = RunGit(['branch', '-r']).split()
456 if 'origin/master' in remote_branches:
457 # Fall back on origin/master if it exits.
458 remote = 'origin'
459 upstream_branch = 'refs/heads/master'
460 elif 'origin/trunk' in remote_branches:
461 # Fall back on origin/trunk if it exists. Generally a shared
462 # git-svn clone
463 remote = 'origin'
464 upstream_branch = 'refs/heads/trunk'
465 else:
466 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000467Either pass complete "git diff"-style arguments, like
468 git cl upload origin/master
469or verify this branch is set up to track another (via the --track argument to
470"git checkout -b ...").""")
471
472 return remote, upstream_branch
473
474 def GetUpstreamBranch(self):
475 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000476 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477 if remote is not '.':
478 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
479 self.upstream_branch = upstream_branch
480 return self.upstream_branch
481
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000482 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000483 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000484 remote, branch = None, self.GetBranch()
485 seen_branches = set()
486 while branch not in seen_branches:
487 seen_branches.add(branch)
488 remote, branch = self.FetchUpstreamTuple(branch)
489 branch = ShortBranchName(branch)
490 if remote != '.' or branch.startswith('refs/remotes'):
491 break
492 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000493 remotes = RunGit(['remote'], error_ok=True).split()
494 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000495 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000496 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000497 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000498 logging.warning('Could not determine which remote this change is '
499 'associated with, so defaulting to "%s". This may '
500 'not be what you want. You may prevent this message '
501 'by running "git svn info" as documented here: %s',
502 self._remote,
503 GIT_INSTRUCTIONS_URL)
504 else:
505 logging.warn('Could not determine which remote this change is '
506 'associated with. You may prevent this message by '
507 'running "git svn info" as documented here: %s',
508 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000509 branch = 'HEAD'
510 if branch.startswith('refs/remotes'):
511 self._remote = (remote, branch)
512 else:
513 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000514 return self._remote
515
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 def GitSanityChecks(self, upstream_git_obj):
517 """Checks git repo status and ensures diff is from local commits."""
518
519 # Verify the commit we're diffing against is in our current branch.
520 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
521 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
522 if upstream_sha != common_ancestor:
523 print >> sys.stderr, (
524 'ERROR: %s is not in the current branch. You may need to rebase '
525 'your tracking branch' % upstream_sha)
526 return False
527
528 # List the commits inside the diff, and verify they are all local.
529 commits_in_diff = RunGit(
530 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
531 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
532 remote_branch = remote_branch.strip()
533 if code != 0:
534 _, remote_branch = self.GetRemoteBranch()
535
536 commits_in_remote = RunGit(
537 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
538
539 common_commits = set(commits_in_diff) & set(commits_in_remote)
540 if common_commits:
541 print >> sys.stderr, (
542 'ERROR: Your diff contains %d commits already in %s.\n'
543 'Run "git log --oneline %s..HEAD" to get a list of commits in '
544 'the diff. If you are using a custom git flow, you can override'
545 ' the reference used for this check with "git config '
546 'gitcl.remotebranch <git-ref>".' % (
547 len(common_commits), remote_branch, upstream_git_obj))
548 return False
549 return True
550
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000551 def GetGitBaseUrlFromConfig(self):
552 """Return the configured base URL from branch.<branchname>.baseurl.
553
554 Returns None if it is not set.
555 """
556 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
557 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000558
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000559 def GetRemoteUrl(self):
560 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
561
562 Returns None if there is no remote.
563 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000564 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000565 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
566
567 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000568 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
571 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000572 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573 else:
574 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 self.has_issue = True
576 return self.issue
577
578 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000579 if not self.rietveld_server:
580 # If we're on a branch then get the server potentially associated
581 # with that branch.
582 if self.GetIssue():
583 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
584 ['config', self._RietveldServer()], error_ok=True).strip())
585 if not self.rietveld_server:
586 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 return self.rietveld_server
588
589 def GetIssueURL(self):
590 """Get the URL for a particular issue."""
591 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
592
593 def GetDescription(self, pretty=False):
594 if not self.has_description:
595 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000596 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000597 try:
598 self.description = self.RpcServer().get_description(issue).strip()
599 except urllib2.HTTPError, e:
600 if e.code == 404:
601 DieWithError(
602 ('\nWhile fetching the description for issue %d, received a '
603 '404 (not found)\n'
604 'error. It is likely that you deleted this '
605 'issue on the server. If this is the\n'
606 'case, please run\n\n'
607 ' git cl issue 0\n\n'
608 'to clear the association with the deleted issue. Then run '
609 'this command again.') % issue)
610 else:
611 DieWithError(
612 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 self.has_description = True
614 if pretty:
615 wrapper = textwrap.TextWrapper()
616 wrapper.initial_indent = wrapper.subsequent_indent = ' '
617 return wrapper.fill(self.description)
618 return self.description
619
620 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000621 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 if not self.has_patchset:
623 patchset = RunGit(['config', self._PatchsetSetting()],
624 error_ok=True).strip()
625 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000626 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 else:
628 self.patchset = None
629 self.has_patchset = True
630 return self.patchset
631
632 def SetPatchset(self, patchset):
633 """Set this branch's patchset. If patchset=0, clears the patchset."""
634 if patchset:
635 RunGit(['config', self._PatchsetSetting(), str(patchset)])
636 else:
637 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000638 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639 self.has_patchset = False
640
binji@chromium.org0281f522012-09-14 13:37:59 +0000641 def GetMostRecentPatchset(self, issue):
642 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000643 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000644
645 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000646 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000647 '/download/issue%s_%s.diff' % (issue, patchset))
648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 def SetIssue(self, issue):
650 """Set this branch's issue. If issue=0, clears the issue."""
651 if issue:
652 RunGit(['config', self._IssueSetting(), str(issue)])
653 if self.rietveld_server:
654 RunGit(['config', self._RietveldServer(), self.rietveld_server])
655 else:
656 RunGit(['config', '--unset', self._IssueSetting()])
657 self.SetPatchset(0)
658 self.has_issue = False
659
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000660 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000661 if not self.GitSanityChecks(upstream_branch):
662 DieWithError('\nGit sanity check failure')
663
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000664 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
665 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000666
667 # We use the sha1 of HEAD as a name of this change.
668 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000669 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000670 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000671 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000672 except subprocess2.CalledProcessError:
673 DieWithError(
674 ('\nFailed to diff against upstream branch %s!\n\n'
675 'This branch probably doesn\'t exist anymore. To reset the\n'
676 'tracking branch, please run\n'
677 ' git branch --set-upstream %s trunk\n'
678 'replacing trunk with origin/master or the relevant branch') %
679 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000680
maruel@chromium.org52424302012-08-29 15:14:30 +0000681 issue = self.GetIssue()
682 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000683 if issue:
684 description = self.GetDescription()
685 else:
686 # If the change was never uploaded, use the log messages of all commits
687 # up to the branch point, as git cl upload will prefill the description
688 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000689 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
690 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000691
692 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000693 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000694 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000695 name,
696 description,
697 absroot,
698 files,
699 issue,
700 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000701 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000702
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000703 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000704 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000705
706 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000707 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000708 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000709 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000710 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000711 except presubmit_support.PresubmitFailure, e:
712 DieWithError(
713 ('%s\nMaybe your depot_tools is out of date?\n'
714 'If all fails, contact maruel@') % e)
715
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000717 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000718 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000719 self.RpcServer().update_description(issue, self.description)
720 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000722 def SetFlag(self, flag, value):
723 """Patchset must match."""
724 if not self.GetPatchset():
725 DieWithError('The patchset needs to match. Send another patchset.')
726 try:
727 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000728 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000729 except urllib2.HTTPError, e:
730 if e.code == 404:
731 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
732 if e.code == 403:
733 DieWithError(
734 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
735 'match?') % (self.GetIssue(), self.GetPatchset()))
736 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000738 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 """Returns an upload.RpcServer() to access this review's rietveld instance.
740 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000741 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000742 self._rpc_server = rietveld.CachingRietveld(
743 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000744 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745
746 def _IssueSetting(self):
747 """Return the git setting that stores this change's issue."""
748 return 'branch.%s.rietveldissue' % self.GetBranch()
749
750 def _PatchsetSetting(self):
751 """Return the git setting that stores this change's most recent patchset."""
752 return 'branch.%s.rietveldpatchset' % self.GetBranch()
753
754 def _RietveldServer(self):
755 """Returns the git setting that stores this change's rietveld server."""
756 return 'branch.%s.rietveldserver' % self.GetBranch()
757
758
759def GetCodereviewSettingsInteractively():
760 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000761 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 server = settings.GetDefaultServerUrl(error_ok=True)
763 prompt = 'Rietveld server (host[:port])'
764 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000765 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 if not server and not newserver:
767 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000768 if newserver:
769 newserver = gclient_utils.UpgradeToHttps(newserver)
770 if newserver != server:
771 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000773 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774 prompt = caption
775 if initial:
776 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000777 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 if new_val == 'x':
779 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000780 elif new_val:
781 if is_url:
782 new_val = gclient_utils.UpgradeToHttps(new_val)
783 if new_val != initial:
784 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000786 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000788 'tree-status-url', False)
789 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790
791 # TODO: configure a default branch to diff against, rather than this
792 # svn-based hackery.
793
794
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000795class ChangeDescription(object):
796 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000797 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000798 self.log_desc = log_desc
799 self.reviewers = reviewers
800 self.description = self.log_desc
801
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000802 def Prompt(self):
803 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000804# This will displayed on the codereview site.
805# The first line will also be used as the subject of the review.
806"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000807 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000808 if ('\nR=' not in self.description and
809 '\nTBR=' not in self.description and
810 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000811 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000812 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000813 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000814 content = content.rstrip('\n') + '\n'
815 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000816 if not content:
817 DieWithError('Running editor failed')
818 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000819 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000820 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000821 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000822
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000823 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000824 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000825 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000826 # Retrieves all reviewer lines
827 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000828 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000829 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000830 if reviewers:
831 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000832
833 def IsEmpty(self):
834 return not self.description
835
836
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837def FindCodereviewSettingsFile(filename='codereview.settings'):
838 """Finds the given file starting in the cwd and going up.
839
840 Only looks up to the top of the repository unless an
841 'inherit-review-settings-ok' file exists in the root of the repository.
842 """
843 inherit_ok_file = 'inherit-review-settings-ok'
844 cwd = os.getcwd()
845 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
846 if os.path.isfile(os.path.join(root, inherit_ok_file)):
847 root = '/'
848 while True:
849 if filename in os.listdir(cwd):
850 if os.path.isfile(os.path.join(cwd, filename)):
851 return open(os.path.join(cwd, filename))
852 if cwd == root:
853 break
854 cwd = os.path.dirname(cwd)
855
856
857def LoadCodereviewSettingsFromFile(fileobj):
858 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000859 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861 def SetProperty(name, setting, unset_error_ok=False):
862 fullname = 'rietveld.' + name
863 if setting in keyvals:
864 RunGit(['config', fullname, keyvals[setting]])
865 else:
866 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
867
868 SetProperty('server', 'CODE_REVIEW_SERVER')
869 # Only server setting is required. Other settings can be absent.
870 # In that case, we ignore errors raised during option deletion attempt.
871 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
872 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
873 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
874
ukai@chromium.orge8077812012-02-03 03:41:46 +0000875 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
876 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
877 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000878
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
880 #should be of the form
881 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
882 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
883 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
884 keyvals['ORIGIN_URL_CONFIG']])
885
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000887def urlretrieve(source, destination):
888 """urllib is broken for SSL connections via a proxy therefore we
889 can't use urllib.urlretrieve()."""
890 with open(destination, 'w') as f:
891 f.write(urllib2.urlopen(source).read())
892
893
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000894def DownloadHooks(force):
895 """downloads hooks
896
897 Args:
898 force: True to update hooks. False to install hooks if not present.
899 """
900 if not settings.GetIsGerrit():
901 return
902 server_url = settings.GetDefaultServerUrl()
903 src = '%s/tools/hooks/commit-msg' % server_url
904 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
905 if not os.access(dst, os.X_OK):
906 if os.path.exists(dst):
907 if not force:
908 return
909 os.remove(dst)
910 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000911 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000912 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
913 except Exception:
914 if os.path.exists(dst):
915 os.remove(dst)
916 DieWithError('\nFailed to download hooks from %s' % src)
917
918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919@usage('[repo root containing codereview.settings]')
920def CMDconfig(parser, args):
921 """edit configuration for this tree"""
922
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000923 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924 if len(args) == 0:
925 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000926 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 return 0
928
929 url = args[0]
930 if not url.endswith('codereview.settings'):
931 url = os.path.join(url, 'codereview.settings')
932
933 # Load code review settings and download hooks (if available).
934 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000935 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 return 0
937
938
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000939def CMDbaseurl(parser, args):
940 """get or set base-url for this branch"""
941 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
942 branch = ShortBranchName(branchref)
943 _, args = parser.parse_args(args)
944 if not args:
945 print("Current base-url:")
946 return RunGit(['config', 'branch.%s.base-url' % branch],
947 error_ok=False).strip()
948 else:
949 print("Setting base-url to %s" % args[0])
950 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
951 error_ok=False).strip()
952
953
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954def CMDstatus(parser, args):
955 """show status of changelists"""
956 parser.add_option('--field',
957 help='print only specific field (desc|id|patch|url)')
958 (options, args) = parser.parse_args(args)
959
960 # TODO: maybe make show_branches a flag if necessary.
961 show_branches = not options.field
962
963 if show_branches:
964 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
965 if branches:
966 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000967 changes = (Changelist(branchref=b) for b in branches.splitlines())
968 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
969 alignment = max(5, max(len(b) for b in branches))
970 for branch in sorted(branches):
971 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972
973 cl = Changelist()
974 if options.field:
975 if options.field.startswith('desc'):
976 print cl.GetDescription()
977 elif options.field == 'id':
978 issueid = cl.GetIssue()
979 if issueid:
980 print issueid
981 elif options.field == 'patch':
982 patchset = cl.GetPatchset()
983 if patchset:
984 print patchset
985 elif options.field == 'url':
986 url = cl.GetIssueURL()
987 if url:
988 print url
989 else:
990 print
991 print 'Current branch:',
992 if not cl.GetIssue():
993 print 'no issue assigned.'
994 return 0
995 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000996 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997 print 'Issue description:'
998 print cl.GetDescription(pretty=True)
999 return 0
1000
1001
1002@usage('[issue_number]')
1003def CMDissue(parser, args):
1004 """Set or display the current code review issue number.
1005
1006 Pass issue number 0 to clear the current issue.
1007"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001008 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009
1010 cl = Changelist()
1011 if len(args) > 0:
1012 try:
1013 issue = int(args[0])
1014 except ValueError:
1015 DieWithError('Pass a number to set the issue or none to list it.\n'
1016 'Maybe you want to run git cl status?')
1017 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001018 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019 return 0
1020
1021
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001022def CMDcomments(parser, args):
1023 """show review comments of the current changelist"""
1024 (_, args) = parser.parse_args(args)
1025 if args:
1026 parser.error('Unsupported argument: %s' % args)
1027
1028 cl = Changelist()
1029 if cl.GetIssue():
1030 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1031 for message in sorted(data['messages'], key=lambda x: x['date']):
1032 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1033 if message['text'].strip():
1034 print '\n'.join(' ' + l for l in message['text'].splitlines())
1035 return 0
1036
1037
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038def CreateDescriptionFromLog(args):
1039 """Pulls out the commit log to use as a base for the CL description."""
1040 log_args = []
1041 if len(args) == 1 and not args[0].endswith('.'):
1042 log_args = [args[0] + '..']
1043 elif len(args) == 1 and args[0].endswith('...'):
1044 log_args = [args[0][:-1]]
1045 elif len(args) == 2:
1046 log_args = [args[0] + '..' + args[1]]
1047 else:
1048 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001049 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050
1051
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052def CMDpresubmit(parser, args):
1053 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001054 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001056 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001057 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 (options, args) = parser.parse_args(args)
1059
ukai@chromium.org259e4682012-10-25 07:36:33 +00001060 if not options.force and is_dirty_git_tree('presubmit'):
1061 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062 return 1
1063
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001064 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065 if args:
1066 base_branch = args[0]
1067 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001068 # Default to diffing against the common ancestor of the upstream branch.
1069 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001071 cl.RunHook(
1072 committing=not options.upload,
1073 may_prompt=False,
1074 verbose=options.verbose,
1075 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001076 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077
1078
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001079def AddChangeIdToCommitMessage(options, args):
1080 """Re-commits using the current message, assumes the commit hook is in
1081 place.
1082 """
1083 log_desc = options.message or CreateDescriptionFromLog(args)
1084 git_command = ['commit', '--amend', '-m', log_desc]
1085 RunGit(git_command)
1086 new_log_desc = CreateDescriptionFromLog(args)
1087 if CHANGE_ID in new_log_desc:
1088 print 'git-cl: Added Change-Id to commit message.'
1089 else:
1090 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1091
1092
ukai@chromium.orge8077812012-02-03 03:41:46 +00001093def GerritUpload(options, args, cl):
1094 """upload the current branch to gerrit."""
1095 # We assume the remote called "origin" is the one we want.
1096 # It is probably not worthwhile to support different workflows.
1097 remote = 'origin'
1098 branch = 'master'
1099 if options.target_branch:
1100 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001102 log_desc = options.message or CreateDescriptionFromLog(args)
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001103 if CHANGE_ID not in log_desc:
1104 AddChangeIdToCommitMessage(options, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001105 if options.reviewers:
1106 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001107 change_desc = ChangeDescription(log_desc, options.reviewers)
1108 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +00001109 if change_desc.IsEmpty():
1110 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 return 1
1112
ukai@chromium.orge8077812012-02-03 03:41:46 +00001113 receive_options = []
1114 cc = cl.GetCCList().split(',')
1115 if options.cc:
1116 cc += options.cc.split(',')
1117 cc = filter(None, cc)
1118 if cc:
1119 receive_options += ['--cc=' + email for email in cc]
1120 if change_desc.reviewers:
1121 reviewers = filter(None, change_desc.reviewers.split(','))
1122 if reviewers:
1123 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124
ukai@chromium.orge8077812012-02-03 03:41:46 +00001125 git_command = ['push']
1126 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001127 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001128 ' '.join(receive_options))
1129 git_command += [remote, 'HEAD:refs/for/' + branch]
1130 RunGit(git_command)
1131 # TODO(ukai): parse Change-Id: and set issue number?
1132 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134
ukai@chromium.orge8077812012-02-03 03:41:46 +00001135def RietveldUpload(options, args, cl):
1136 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1138 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 if options.emulate_svn_auto_props:
1140 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141
1142 change_desc = None
1143
1144 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001145 if options.title:
1146 upload_args.extend(['--title', options.title])
1147 elif options.message:
1148 # TODO(rogerta): for now, the -m option will also set the --title option
1149 # for upload.py. Soon this will be changed to set the --message option.
1150 # Will wait until people are used to typing -t instead of -m.
1151 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001152 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 print ("This branch is associated with issue %s. "
1154 "Adding patch to that issue." % cl.GetIssue())
1155 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001156 if options.title:
1157 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001158 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001159 change_desc = ChangeDescription(message, options.reviewers)
1160 if not options.force:
1161 change_desc.Prompt()
1162 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001163
1164 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 print "Description is empty; aborting."
1166 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001167
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001168 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001169 if change_desc.reviewers:
1170 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001171 if options.send_mail:
1172 if not change_desc.reviewers:
1173 DieWithError("Must specify reviewers to send email.")
1174 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001175 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001176 if cc:
1177 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001179 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001180 if not options.find_copies:
1181 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001182
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 # Include the upstream repo's URL in the change -- this is useful for
1184 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001185 remote_url = cl.GetGitBaseUrlFromConfig()
1186 if not remote_url:
1187 if settings.GetIsGitSvn():
1188 # URL is dependent on the current directory.
1189 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1190 if data:
1191 keys = dict(line.split(': ', 1) for line in data.splitlines()
1192 if ': ' in line)
1193 remote_url = keys.get('URL', None)
1194 else:
1195 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1196 remote_url = (cl.GetRemoteUrl() + '@'
1197 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 if remote_url:
1199 upload_args.extend(['--base_url', remote_url])
1200
1201 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001202 upload_args = ['upload'] + upload_args + args
1203 logging.info('upload.RealMain(%s)', upload_args)
1204 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001205 except KeyboardInterrupt:
1206 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 except:
1208 # If we got an exception after the user typed a description for their
1209 # change, back up the description before re-raising.
1210 if change_desc:
1211 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1212 print '\nGot exception while uploading -- saving description to %s\n' \
1213 % backup_path
1214 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001215 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216 backup_file.close()
1217 raise
1218
1219 if not cl.GetIssue():
1220 cl.SetIssue(issue)
1221 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001222
1223 if options.use_commit_queue:
1224 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 return 0
1226
1227
ukai@chromium.orge8077812012-02-03 03:41:46 +00001228@usage('[args to "git diff"]')
1229def CMDupload(parser, args):
1230 """upload the current changelist to codereview"""
1231 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1232 help='bypass upload presubmit hook')
1233 parser.add_option('-f', action='store_true', dest='force',
1234 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001235 parser.add_option('-m', dest='message', help='message for patchset')
1236 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001237 parser.add_option('-r', '--reviewers',
1238 help='reviewer email addresses')
1239 parser.add_option('--cc',
1240 help='cc email addresses')
1241 parser.add_option('--send-mail', action='store_true',
1242 help='send email to reviewer immediately')
1243 parser.add_option("--emulate_svn_auto_props", action="store_true",
1244 dest="emulate_svn_auto_props",
1245 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001246 parser.add_option('-c', '--use-commit-queue', action='store_true',
1247 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001248 parser.add_option('--target_branch',
1249 help='When uploading to gerrit, remote branch to '
1250 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001251 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001252 (options, args) = parser.parse_args(args)
1253
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001254 if options.target_branch and not settings.GetIsGerrit():
1255 parser.error('Use --target_branch for non gerrit repository.')
1256
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001257 # Print warning if the user used the -m/--message argument. This will soon
1258 # change to -t/--title.
1259 if options.message:
1260 print >> sys.stderr, (
1261 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1262 'In the near future, -m or --message will send a message instead.\n'
1263 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001264
ukai@chromium.org259e4682012-10-25 07:36:33 +00001265 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001266 return 1
1267
1268 cl = Changelist()
1269 if args:
1270 # TODO(ukai): is it ok for gerrit case?
1271 base_branch = args[0]
1272 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 # Default to diffing against common ancestor of upstream branch
1274 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001275 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001276
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001277 # Apply watchlists on upload.
1278 change = cl.GetChange(base_branch, None)
1279 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1280 files = [f.LocalPath() for f in change.AffectedFiles()]
1281 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1282
ukai@chromium.orge8077812012-02-03 03:41:46 +00001283 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001284 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001285 may_prompt=not options.force,
1286 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001287 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001288 if not hook_results.should_continue():
1289 return 1
1290 if not options.reviewers and hook_results.reviewers:
1291 options.reviewers = hook_results.reviewers
1292
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001293 if cl.GetIssue():
1294 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1295 local_patchset = cl.GetPatchset()
1296 if local_patchset != latest_patchset:
1297 print ('The last upload made from this repository was patchset #%d but '
1298 'the most recent patchset on the server is #%d.'
1299 % (local_patchset, latest_patchset))
1300 ask_for_data('About to upload; enter to confirm.')
1301
iannucci@chromium.org79540052012-10-19 23:15:26 +00001302 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001303 if settings.GetIsGerrit():
1304 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001305 ret = RietveldUpload(options, args, cl)
1306 if not ret:
1307 git_set_branch_value('last-upload-hash', RunGit(['rev-parse', 'HEAD']))
1308
1309 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001310
1311
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001312def IsSubmoduleMergeCommit(ref):
1313 # When submodules are added to the repo, we expect there to be a single
1314 # non-git-svn merge commit at remote HEAD with a signature comment.
1315 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001316 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001317 return RunGit(cmd) != ''
1318
1319
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320def SendUpstream(parser, args, cmd):
1321 """Common code for CmdPush and CmdDCommit
1322
1323 Squashed commit into a single.
1324 Updates changelog with metadata (e.g. pointer to review).
1325 Pushes/dcommits the code upstream.
1326 Updates review and closes.
1327 """
1328 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1329 help='bypass upload presubmit hook')
1330 parser.add_option('-m', dest='message',
1331 help="override review description")
1332 parser.add_option('-f', action='store_true', dest='force',
1333 help="force yes to questions (don't prompt)")
1334 parser.add_option('-c', dest='contributor',
1335 help="external contributor for patch (appended to " +
1336 "description and used as author for git). Should be " +
1337 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001338 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 (options, args) = parser.parse_args(args)
1340 cl = Changelist()
1341
1342 if not args or cmd == 'push':
1343 # Default to merging against our best guess of the upstream branch.
1344 args = [cl.GetUpstreamBranch()]
1345
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001346 if options.contributor:
1347 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1348 print "Please provide contibutor as 'First Last <email@example.com>'"
1349 return 1
1350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001352 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353
ukai@chromium.org259e4682012-10-25 07:36:33 +00001354 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 return 1
1356
1357 # This rev-list syntax means "show all commits not in my branch that
1358 # are in base_branch".
1359 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1360 base_branch]).splitlines()
1361 if upstream_commits:
1362 print ('Base branch "%s" has %d commits '
1363 'not in this branch.' % (base_branch, len(upstream_commits)))
1364 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1365 return 1
1366
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001367 # This is the revision `svn dcommit` will commit on top of.
1368 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1369 '--pretty=format:%H'])
1370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001372 # If the base_head is a submodule merge commit, the first parent of the
1373 # base_head should be a git-svn commit, which is what we're interested in.
1374 base_svn_head = base_branch
1375 if base_has_submodules:
1376 base_svn_head += '^1'
1377
1378 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 if extra_commits:
1380 print ('This branch has %d additional commits not upstreamed yet.'
1381 % len(extra_commits.splitlines()))
1382 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1383 'before attempting to %s.' % (base_branch, cmd))
1384 return 1
1385
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001386 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001387 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001388 author = None
1389 if options.contributor:
1390 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001391 hook_results = cl.RunHook(
1392 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001393 may_prompt=not options.force,
1394 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001395 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001396 if not hook_results.should_continue():
1397 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398
1399 if cmd == 'dcommit':
1400 # Check the tree status if the tree status URL is set.
1401 status = GetTreeStatus()
1402 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001403 print('The tree is closed. Please wait for it to reopen. Use '
1404 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 return 1
1406 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001407 print('Unable to determine tree status. Please verify manually and '
1408 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001409 else:
1410 breakpad.SendStack(
1411 'GitClHooksBypassedCommit',
1412 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001413 (cl.GetRietveldServer(), cl.GetIssue()),
1414 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
1416 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001417 if not description and cl.GetIssue():
1418 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001420 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001421 if not cl.GetIssue() and options.bypass_hooks:
1422 description = CreateDescriptionFromLog([base_branch])
1423 else:
1424 print 'No description set.'
1425 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1426 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001428 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430
1431 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432 description += "\nPatch from %s." % options.contributor
1433 print 'Description:', repr(description)
1434
1435 branches = [base_branch, cl.GetBranchRef()]
1436 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001437 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001438 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001440 # We want to squash all this branch's commits into one commit with the proper
1441 # description. We do this by doing a "reset --soft" to the base branch (which
1442 # keeps the working copy the same), then dcommitting that. If origin/master
1443 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1444 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001446 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1447 # Delete the branches if they exist.
1448 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1449 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1450 result = RunGitWithCode(showref_cmd)
1451 if result[0] == 0:
1452 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453
1454 # We might be in a directory that's present in this branch but not in the
1455 # trunk. Move up to the top of the tree so that git commands that expect a
1456 # valid CWD won't fail after we check out the merge branch.
1457 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1458 if rel_base_path:
1459 os.chdir(rel_base_path)
1460
1461 # Stuff our change into the merge branch.
1462 # We wrap in a try...finally block so if anything goes wrong,
1463 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001464 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001466 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1467 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 if options.contributor:
1469 RunGit(['commit', '--author', options.contributor, '-m', description])
1470 else:
1471 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001472 if base_has_submodules:
1473 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1474 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1475 RunGit(['checkout', CHERRY_PICK_BRANCH])
1476 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 if cmd == 'push':
1478 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001479 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480 retcode, output = RunGitWithCode(
1481 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1482 logging.debug(output)
1483 else:
1484 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001485 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001486 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001487 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 finally:
1489 # And then swap back to the original branch and clean up.
1490 RunGit(['checkout', '-q', cl.GetBranch()])
1491 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001492 if base_has_submodules:
1493 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001494
1495 if cl.GetIssue():
1496 if cmd == 'dcommit' and 'Committed r' in output:
1497 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1498 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001499 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1500 for l in output.splitlines(False))
1501 match = filter(None, match)
1502 if len(match) != 1:
1503 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1504 output)
1505 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 else:
1507 return 1
1508 viewvc_url = settings.GetViewVCUrl()
1509 if viewvc_url and revision:
1510 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001511 elif revision:
1512 cl.description += ('\n\nCommitted: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513 print ('Closing issue '
1514 '(you may be prompted for your codereview password)...')
1515 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001516 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001517 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001518 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001519 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1520 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001522
1523 if retcode == 0:
1524 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1525 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001526 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001527
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001528 return 0
1529
1530
1531@usage('[upstream branch to apply against]')
1532def CMDdcommit(parser, args):
1533 """commit the current changelist via git-svn"""
1534 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001535 message = """This doesn't appear to be an SVN repository.
1536If your project has a git mirror with an upstream SVN master, you probably need
1537to run 'git svn init', see your project's git mirror documentation.
1538If your project has a true writeable upstream repository, you probably want
1539to run 'git cl push' instead.
1540Choose wisely, if you get this wrong, your commit might appear to succeed but
1541will instead be silently ignored."""
1542 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001543 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544 return SendUpstream(parser, args, 'dcommit')
1545
1546
1547@usage('[upstream branch to apply against]')
1548def CMDpush(parser, args):
1549 """commit the current changelist via git"""
1550 if settings.GetIsGitSvn():
1551 print('This appears to be an SVN repository.')
1552 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001553 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554 return SendUpstream(parser, args, 'push')
1555
1556
1557@usage('<patch url or issue id>')
1558def CMDpatch(parser, args):
1559 """patch in a code review"""
1560 parser.add_option('-b', dest='newbranch',
1561 help='create a new branch off trunk for the patch')
1562 parser.add_option('-f', action='store_true', dest='force',
1563 help='with -b, clobber any existing branch')
1564 parser.add_option('--reject', action='store_true', dest='reject',
1565 help='allow failed patches and spew .rej files')
1566 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1567 help="don't commit after patch applies")
1568 (options, args) = parser.parse_args(args)
1569 if len(args) != 1:
1570 parser.print_help()
1571 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001572 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001573
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001574 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001575 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001576
maruel@chromium.org52424302012-08-29 15:14:30 +00001577 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001579 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001580 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001581 patchset = cl.GetMostRecentPatchset(issue)
1582 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001584 # Assume it's a URL to the patch. Default to https.
1585 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001586 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001587 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588 DieWithError('Must pass an issue ID or full URL for '
1589 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001590 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001591 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001592 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593
1594 if options.newbranch:
1595 if options.force:
1596 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001597 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001598 RunGit(['checkout', '-b', options.newbranch,
1599 Changelist().GetUpstreamBranch()])
1600
1601 # Switch up to the top-level directory, if necessary, in preparation for
1602 # applying the patch.
1603 top = RunGit(['rev-parse', '--show-cdup']).strip()
1604 if top:
1605 os.chdir(top)
1606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607 # Git patches have a/ at the beginning of source paths. We strip that out
1608 # with a sed script rather than the -p flag to patch so we can feed either
1609 # Git or svn-style patches into the same apply command.
1610 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001611 try:
1612 patch_data = subprocess2.check_output(
1613 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1614 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001615 DieWithError('Git patch mungling failed.')
1616 logging.info(patch_data)
1617 # We use "git apply" to apply the patch instead of "patch" so that we can
1618 # pick up file adds.
1619 # The --index flag means: also insert into the index (so we catch adds).
1620 cmd = ['git', 'apply', '--index', '-p0']
1621 if options.reject:
1622 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001623 try:
1624 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1625 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626 DieWithError('Failed to apply the patch')
1627
1628 # If we had an issue, commit the current state and register the issue.
1629 if not options.nocommit:
1630 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1631 cl = Changelist()
1632 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001633 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001634 print "Committed patch."
1635 else:
1636 print "Patch applied to index."
1637 return 0
1638
1639
1640def CMDrebase(parser, args):
1641 """rebase current branch on top of svn repo"""
1642 # Provide a wrapper for git svn rebase to help avoid accidental
1643 # git svn dcommit.
1644 # It's the only command that doesn't use parser at all since we just defer
1645 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001646 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647
1648
1649def GetTreeStatus():
1650 """Fetches the tree status and returns either 'open', 'closed',
1651 'unknown' or 'unset'."""
1652 url = settings.GetTreeStatusUrl(error_ok=True)
1653 if url:
1654 status = urllib2.urlopen(url).read().lower()
1655 if status.find('closed') != -1 or status == '0':
1656 return 'closed'
1657 elif status.find('open') != -1 or status == '1':
1658 return 'open'
1659 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001660 return 'unset'
1661
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663def GetTreeStatusReason():
1664 """Fetches the tree status from a json url and returns the message
1665 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001666 url = settings.GetTreeStatusUrl()
1667 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001668 connection = urllib2.urlopen(json_url)
1669 status = json.loads(connection.read())
1670 connection.close()
1671 return status['message']
1672
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001673
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674def CMDtree(parser, args):
1675 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001676 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677 status = GetTreeStatus()
1678 if 'unset' == status:
1679 print 'You must configure your tree status URL by running "git cl config".'
1680 return 2
1681
1682 print "The tree is %s" % status
1683 print
1684 print GetTreeStatusReason()
1685 if status != 'open':
1686 return 1
1687 return 0
1688
1689
maruel@chromium.org15192402012-09-06 12:38:29 +00001690def CMDtry(parser, args):
1691 """Triggers a try job through Rietveld."""
1692 group = optparse.OptionGroup(parser, "Try job options")
1693 group.add_option(
1694 "-b", "--bot", action="append",
1695 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1696 "times to specify multiple builders. ex: "
1697 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1698 "the try server waterfall for the builders name and the tests "
1699 "available. Can also be used to specify gtest_filter, e.g. "
1700 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1701 group.add_option(
1702 "-r", "--revision",
1703 help="Revision to use for the try job; default: the "
1704 "revision will be determined by the try server; see "
1705 "its waterfall for more info")
1706 group.add_option(
1707 "-c", "--clobber", action="store_true", default=False,
1708 help="Force a clobber before building; e.g. don't do an "
1709 "incremental build")
1710 group.add_option(
1711 "--project",
1712 help="Override which project to use. Projects are defined "
1713 "server-side to define what default bot set to use")
1714 group.add_option(
1715 "-t", "--testfilter", action="append", default=[],
1716 help=("Apply a testfilter to all the selected builders. Unless the "
1717 "builders configurations are similar, use multiple "
1718 "--bot <builder>:<test> arguments."))
1719 group.add_option(
1720 "-n", "--name", help="Try job name; default to current branch name")
1721 parser.add_option_group(group)
1722 options, args = parser.parse_args(args)
1723
1724 if args:
1725 parser.error('Unknown arguments: %s' % args)
1726
1727 cl = Changelist()
1728 if not cl.GetIssue():
1729 parser.error('Need to upload first')
1730
1731 if not options.name:
1732 options.name = cl.GetBranch()
1733
1734 # Process --bot and --testfilter.
1735 if not options.bot:
1736 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001737 change = cl.GetChange(
1738 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1739 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001740 options.bot = presubmit_support.DoGetTrySlaves(
1741 change,
1742 change.LocalPaths(),
1743 settings.GetRoot(),
1744 None,
1745 None,
1746 options.verbose,
1747 sys.stdout)
1748 if not options.bot:
1749 parser.error('No default try builder to try, use --bot')
1750
1751 builders_and_tests = {}
1752 for bot in options.bot:
1753 if ':' in bot:
1754 builder, tests = bot.split(':', 1)
1755 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1756 elif ',' in bot:
1757 parser.error('Specify one bot per --bot flag')
1758 else:
1759 builders_and_tests.setdefault(bot, []).append('defaulttests')
1760
1761 if options.testfilter:
1762 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1763 builders_and_tests = dict(
1764 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1765 if t != ['compile'])
1766
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001767 if any('triggered' in b for b in builders_and_tests):
1768 print >> sys.stderr, (
1769 'ERROR You are trying to send a job to a triggered bot. This type of'
1770 ' bot requires an\ninitial job from a parent (usually a builder). '
1771 'Instead send your job to the parent.\n'
1772 'Bot list: %s' % builders_and_tests)
1773 return 1
1774
maruel@chromium.org15192402012-09-06 12:38:29 +00001775 patchset = cl.GetPatchset()
1776 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001777 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001778
1779 cl.RpcServer().trigger_try_jobs(
1780 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1781 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001782 print('Tried jobs on:')
1783 length = max(len(builder) for builder in builders_and_tests)
1784 for builder in sorted(builders_and_tests):
1785 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001786 return 0
1787
1788
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001789@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001791 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001792 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001793 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001794 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001795 return 0
1796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001798 if args:
1799 # One arg means set upstream branch.
1800 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1801 cl = Changelist()
1802 print "Upstream branch set to " + cl.GetUpstreamBranch()
1803 else:
1804 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805 return 0
1806
1807
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001808def CMDset_commit(parser, args):
1809 """set the commit bit"""
1810 _, args = parser.parse_args(args)
1811 if args:
1812 parser.error('Unrecognized args: %s' % ' '.join(args))
1813 cl = Changelist()
1814 cl.SetFlag('commit', '1')
1815 return 0
1816
1817
groby@chromium.org411034a2013-02-26 15:12:01 +00001818def CMDset_close(parser, args):
1819 """close the issue"""
1820 _, args = parser.parse_args(args)
1821 if args:
1822 parser.error('Unrecognized args: %s' % ' '.join(args))
1823 cl = Changelist()
1824 # Ensure there actually is an issue to close.
1825 cl.GetDescription()
1826 cl.CloseIssue()
1827 return 0
1828
1829
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830def Command(name):
1831 return getattr(sys.modules[__name__], 'CMD' + name, None)
1832
1833
1834def CMDhelp(parser, args):
1835 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001836 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001837 if len(args) == 1:
1838 return main(args + ['--help'])
1839 parser.print_help()
1840 return 0
1841
1842
1843def GenUsage(parser, command):
1844 """Modify an OptParse object with the function's documentation."""
1845 obj = Command(command)
1846 more = getattr(obj, 'usage_more', '')
1847 if command == 'help':
1848 command = '<command>'
1849 else:
1850 # OptParser.description prefer nicely non-formatted strings.
1851 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1852 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1853
1854
1855def main(argv):
1856 """Doesn't parse the arguments here, just find the right subcommand to
1857 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001858 if sys.hexversion < 0x02060000:
1859 print >> sys.stderr, (
1860 '\nYour python version %s is unsupported, please upgrade.\n' %
1861 sys.version.split(' ', 1)[0])
1862 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001863 # Reload settings.
1864 global settings
1865 settings = Settings()
1866
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867 # Do it late so all commands are listed.
1868 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1869 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1870 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1871
1872 # Create the option parse and add --verbose support.
1873 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001874 parser.add_option(
1875 '-v', '--verbose', action='count', default=0,
1876 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001877 old_parser_args = parser.parse_args
1878 def Parse(args):
1879 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001880 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001882 elif options.verbose:
1883 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884 else:
1885 logging.basicConfig(level=logging.WARNING)
1886 return options, args
1887 parser.parse_args = Parse
1888
1889 if argv:
1890 command = Command(argv[0])
1891 if command:
1892 # "fix" the usage and the description now that we know the subcommand.
1893 GenUsage(parser, argv[0])
1894 try:
1895 return command(parser, argv[1:])
1896 except urllib2.HTTPError, e:
1897 if e.code != 500:
1898 raise
1899 DieWithError(
1900 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1901 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1902
1903 # Not a known command. Default to help.
1904 GenUsage(parser, 'help')
1905 return CMDhelp(parser, argv)
1906
1907
1908if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001909 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 sys.exit(main(sys.argv[1:]))