blob: 60318625dd06823e21b08ce70d258aa5862cd360 [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()
98 if branch:
99 git_key = 'branch.%s.%s' % (branch, key)
100 RunGit(['config', '--int', git_key, "%d" % value])
101
102
103def git_get_branch_default(key, default):
104 branch = Changelist().GetBranch()
105 if branch:
106 git_key = 'branch.%s.%s' % (branch, key)
107 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
108 try:
109 return int(stdout.strip())
110 except ValueError:
111 pass
112 return default
113
114
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000115def add_git_similarity(parser):
116 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000117 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000118 help='Sets the percentage that a pair of files need to match in order to'
119 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000120 parser.add_option(
121 '--find-copies', action='store_true',
122 help='Allows git to look for copies.')
123 parser.add_option(
124 '--no-find-copies', action='store_false', dest='find_copies',
125 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000126
127 old_parser_args = parser.parse_args
128 def Parse(args):
129 options, args = old_parser_args(args)
130
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000131 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000132 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000133 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000134 print('Note: Saving similarity of %d%% in git config.'
135 % options.similarity)
136 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138 options.similarity = max(0, min(options.similarity, 100))
139
140 if options.find_copies is None:
141 options.find_copies = bool(
142 git_get_branch_default('git-find-copies', True))
143 else:
144 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000145
146 print('Using %d%% similarity for rename/copy detection. '
147 'Override with --similarity.' % options.similarity)
148
149 return options, args
150 parser.parse_args = Parse
151
152
ukai@chromium.org259e4682012-10-25 07:36:33 +0000153def is_dirty_git_tree(cmd):
154 # Make sure index is up-to-date before running diff-index.
155 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
156 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
157 if dirty:
158 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
159 print 'Uncommitted files: (git diff-index --name-status HEAD)'
160 print dirty[:4096]
161 if len(dirty) > 4096:
162 print '... (run "git diff-index --name-status HEAD" to see full output).'
163 return True
164 return False
165
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000166
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000167def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
168 """Return the corresponding git ref if |base_url| together with |glob_spec|
169 matches the full |url|.
170
171 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
172 """
173 fetch_suburl, as_ref = glob_spec.split(':')
174 if allow_wildcards:
175 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
176 if glob_match:
177 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
178 # "branches/{472,597,648}/src:refs/remotes/svn/*".
179 branch_re = re.escape(base_url)
180 if glob_match.group(1):
181 branch_re += '/' + re.escape(glob_match.group(1))
182 wildcard = glob_match.group(2)
183 if wildcard == '*':
184 branch_re += '([^/]*)'
185 else:
186 # Escape and replace surrounding braces with parentheses and commas
187 # with pipe symbols.
188 wildcard = re.escape(wildcard)
189 wildcard = re.sub('^\\\\{', '(', wildcard)
190 wildcard = re.sub('\\\\,', '|', wildcard)
191 wildcard = re.sub('\\\\}$', ')', wildcard)
192 branch_re += wildcard
193 if glob_match.group(3):
194 branch_re += re.escape(glob_match.group(3))
195 match = re.match(branch_re, url)
196 if match:
197 return re.sub('\*$', match.group(1), as_ref)
198
199 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
200 if fetch_suburl:
201 full_url = base_url + '/' + fetch_suburl
202 else:
203 full_url = base_url
204 if full_url == url:
205 return as_ref
206 return None
207
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000208
iannucci@chromium.org79540052012-10-19 23:15:26 +0000209def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000210 """Prints statistics about the change to the user."""
211 # --no-ext-diff is broken in some versions of Git, so try to work around
212 # this by overriding the environment (but there is still a problem if the
213 # git config key "diff.external" is used).
214 env = os.environ.copy()
215 if 'GIT_EXTERNAL_DIFF' in env:
216 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000217
218 if find_copies:
219 similarity_options = ['--find-copies-harder', '-l100000',
220 '-C%s' % similarity]
221 else:
222 similarity_options = ['-M%s' % similarity]
223
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000224 return subprocess2.call(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000225 ['git', 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
226 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000227
228
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000229class Settings(object):
230 def __init__(self):
231 self.default_server = None
232 self.cc = None
233 self.root = None
234 self.is_git_svn = None
235 self.svn_branch = None
236 self.tree_status_url = None
237 self.viewvc_url = None
238 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000239 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000240
241 def LazyUpdateIfNeeded(self):
242 """Updates the settings from a codereview.settings file, if available."""
243 if not self.updated:
244 cr_settings_file = FindCodereviewSettingsFile()
245 if cr_settings_file:
246 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000247 self.updated = True
248 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000249 self.updated = True
250
251 def GetDefaultServerUrl(self, error_ok=False):
252 if not self.default_server:
253 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000254 self.default_server = gclient_utils.UpgradeToHttps(
255 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000256 if error_ok:
257 return self.default_server
258 if not self.default_server:
259 error_message = ('Could not find settings file. You must configure '
260 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000261 self.default_server = gclient_utils.UpgradeToHttps(
262 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000263 return self.default_server
264
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000265 def GetRoot(self):
266 if not self.root:
267 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
268 return self.root
269
270 def GetIsGitSvn(self):
271 """Return true if this repo looks like it's using git-svn."""
272 if self.is_git_svn is None:
273 # If you have any "svn-remote.*" config keys, we think you're using svn.
274 self.is_git_svn = RunGitWithCode(
275 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
276 return self.is_git_svn
277
278 def GetSVNBranch(self):
279 if self.svn_branch is None:
280 if not self.GetIsGitSvn():
281 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
282
283 # Try to figure out which remote branch we're based on.
284 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000285 # 1) iterate through our branch history and find the svn URL.
286 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000287
288 # regexp matching the git-svn line that contains the URL.
289 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
290
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000291 # We don't want to go through all of history, so read a line from the
292 # pipe at a time.
293 # The -100 is an arbitrary limit so we don't search forever.
294 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000295 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000296 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000297 for line in proc.stdout:
298 match = git_svn_re.match(line)
299 if match:
300 url = match.group(1)
301 proc.stdout.close() # Cut pipe.
302 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000304 if url:
305 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
306 remotes = RunGit(['config', '--get-regexp',
307 r'^svn-remote\..*\.url']).splitlines()
308 for remote in remotes:
309 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000311 remote = match.group(1)
312 base_url = match.group(2)
313 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000314 ['config', 'svn-remote.%s.fetch' % remote],
315 error_ok=True).strip()
316 if fetch_spec:
317 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
318 if self.svn_branch:
319 break
320 branch_spec = RunGit(
321 ['config', 'svn-remote.%s.branches' % remote],
322 error_ok=True).strip()
323 if branch_spec:
324 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
325 if self.svn_branch:
326 break
327 tag_spec = RunGit(
328 ['config', 'svn-remote.%s.tags' % remote],
329 error_ok=True).strip()
330 if tag_spec:
331 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
332 if self.svn_branch:
333 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000334
335 if not self.svn_branch:
336 DieWithError('Can\'t guess svn branch -- try specifying it on the '
337 'command line')
338
339 return self.svn_branch
340
341 def GetTreeStatusUrl(self, error_ok=False):
342 if not self.tree_status_url:
343 error_message = ('You must configure your tree status URL by running '
344 '"git cl config".')
345 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
346 error_ok=error_ok,
347 error_message=error_message)
348 return self.tree_status_url
349
350 def GetViewVCUrl(self):
351 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000352 self.viewvc_url = gclient_utils.UpgradeToHttps(
353 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000354 return self.viewvc_url
355
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000356 def GetDefaultCCList(self):
357 return self._GetConfig('rietveld.cc', error_ok=True)
358
ukai@chromium.orge8077812012-02-03 03:41:46 +0000359 def GetIsGerrit(self):
360 """Return true if this repo is assosiated with gerrit code review system."""
361 if self.is_gerrit is None:
362 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
363 return self.is_gerrit
364
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365 def _GetConfig(self, param, **kwargs):
366 self.LazyUpdateIfNeeded()
367 return RunGit(['config', param], **kwargs).strip()
368
369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000370def ShortBranchName(branch):
371 """Convert a name like 'refs/heads/foo' to just 'foo'."""
372 return branch.replace('refs/heads/', '')
373
374
375class Changelist(object):
376 def __init__(self, branchref=None):
377 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000378 global settings
379 if not settings:
380 # Happens when git_cl.py is used as a utility library.
381 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000382 settings.GetDefaultServerUrl()
383 self.branchref = branchref
384 if self.branchref:
385 self.branch = ShortBranchName(self.branchref)
386 else:
387 self.branch = None
388 self.rietveld_server = None
389 self.upstream_branch = None
390 self.has_issue = False
391 self.issue = None
392 self.has_description = False
393 self.description = None
394 self.has_patchset = False
395 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000396 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000397 self.cc = None
398 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000399 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000400
401 def GetCCList(self):
402 """Return the users cc'd on this CL.
403
404 Return is a string suitable for passing to gcl with the --cc flag.
405 """
406 if self.cc is None:
407 base_cc = settings .GetDefaultCCList()
408 more_cc = ','.join(self.watchers)
409 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
410 return self.cc
411
412 def SetWatchers(self, watchers):
413 """Set the list of email addresses that should be cc'd based on the changed
414 files in this CL.
415 """
416 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000417
418 def GetBranch(self):
419 """Returns the short branch name, e.g. 'master'."""
420 if not self.branch:
421 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
422 self.branch = ShortBranchName(self.branchref)
423 return self.branch
424
425 def GetBranchRef(self):
426 """Returns the full branch name, e.g. 'refs/heads/master'."""
427 self.GetBranch() # Poke the lazy loader.
428 return self.branchref
429
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000430 @staticmethod
431 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000432 """Returns a tuple containg remote and remote ref,
433 e.g. 'origin', 'refs/heads/master'
434 """
435 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
437 error_ok=True).strip()
438 if upstream_branch:
439 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
440 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000441 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
442 error_ok=True).strip()
443 if upstream_branch:
444 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000445 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000446 # Fall back on trying a git-svn upstream branch.
447 if settings.GetIsGitSvn():
448 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000450 # Else, try to guess the origin remote.
451 remote_branches = RunGit(['branch', '-r']).split()
452 if 'origin/master' in remote_branches:
453 # Fall back on origin/master if it exits.
454 remote = 'origin'
455 upstream_branch = 'refs/heads/master'
456 elif 'origin/trunk' in remote_branches:
457 # Fall back on origin/trunk if it exists. Generally a shared
458 # git-svn clone
459 remote = 'origin'
460 upstream_branch = 'refs/heads/trunk'
461 else:
462 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000463Either pass complete "git diff"-style arguments, like
464 git cl upload origin/master
465or verify this branch is set up to track another (via the --track argument to
466"git checkout -b ...").""")
467
468 return remote, upstream_branch
469
470 def GetUpstreamBranch(self):
471 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000472 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000473 if remote is not '.':
474 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
475 self.upstream_branch = upstream_branch
476 return self.upstream_branch
477
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000478 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000479 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000480 remote, branch = None, self.GetBranch()
481 seen_branches = set()
482 while branch not in seen_branches:
483 seen_branches.add(branch)
484 remote, branch = self.FetchUpstreamTuple(branch)
485 branch = ShortBranchName(branch)
486 if remote != '.' or branch.startswith('refs/remotes'):
487 break
488 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000489 remotes = RunGit(['remote'], error_ok=True).split()
490 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000491 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000492 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000493 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000494 logging.warning('Could not determine which remote this change is '
495 'associated with, so defaulting to "%s". This may '
496 'not be what you want. You may prevent this message '
497 'by running "git svn info" as documented here: %s',
498 self._remote,
499 GIT_INSTRUCTIONS_URL)
500 else:
501 logging.warn('Could not determine which remote this change is '
502 'associated with. You may prevent this message by '
503 'running "git svn info" as documented here: %s',
504 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000505 branch = 'HEAD'
506 if branch.startswith('refs/remotes'):
507 self._remote = (remote, branch)
508 else:
509 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000510 return self._remote
511
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000512 def GitSanityChecks(self, upstream_git_obj):
513 """Checks git repo status and ensures diff is from local commits."""
514
515 # Verify the commit we're diffing against is in our current branch.
516 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
517 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
518 if upstream_sha != common_ancestor:
519 print >> sys.stderr, (
520 'ERROR: %s is not in the current branch. You may need to rebase '
521 'your tracking branch' % upstream_sha)
522 return False
523
524 # List the commits inside the diff, and verify they are all local.
525 commits_in_diff = RunGit(
526 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
527 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
528 remote_branch = remote_branch.strip()
529 if code != 0:
530 _, remote_branch = self.GetRemoteBranch()
531
532 commits_in_remote = RunGit(
533 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
534
535 common_commits = set(commits_in_diff) & set(commits_in_remote)
536 if common_commits:
537 print >> sys.stderr, (
538 'ERROR: Your diff contains %d commits already in %s.\n'
539 'Run "git log --oneline %s..HEAD" to get a list of commits in '
540 'the diff. If you are using a custom git flow, you can override'
541 ' the reference used for this check with "git config '
542 'gitcl.remotebranch <git-ref>".' % (
543 len(common_commits), remote_branch, upstream_git_obj))
544 return False
545 return True
546
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000547 def GetGitBaseUrlFromConfig(self):
548 """Return the configured base URL from branch.<branchname>.baseurl.
549
550 Returns None if it is not set.
551 """
552 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
553 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000554
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000555 def GetRemoteUrl(self):
556 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
557
558 Returns None if there is no remote.
559 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000560 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000561 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
562
563 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000564 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000565 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000566 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
567 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000568 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 else:
570 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 self.has_issue = True
572 return self.issue
573
574 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000575 if not self.rietveld_server:
576 # If we're on a branch then get the server potentially associated
577 # with that branch.
578 if self.GetIssue():
579 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
580 ['config', self._RietveldServer()], error_ok=True).strip())
581 if not self.rietveld_server:
582 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 return self.rietveld_server
584
585 def GetIssueURL(self):
586 """Get the URL for a particular issue."""
587 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
588
589 def GetDescription(self, pretty=False):
590 if not self.has_description:
591 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000592 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000593 try:
594 self.description = self.RpcServer().get_description(issue).strip()
595 except urllib2.HTTPError, e:
596 if e.code == 404:
597 DieWithError(
598 ('\nWhile fetching the description for issue %d, received a '
599 '404 (not found)\n'
600 'error. It is likely that you deleted this '
601 'issue on the server. If this is the\n'
602 'case, please run\n\n'
603 ' git cl issue 0\n\n'
604 'to clear the association with the deleted issue. Then run '
605 'this command again.') % issue)
606 else:
607 DieWithError(
608 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000609 self.has_description = True
610 if pretty:
611 wrapper = textwrap.TextWrapper()
612 wrapper.initial_indent = wrapper.subsequent_indent = ' '
613 return wrapper.fill(self.description)
614 return self.description
615
616 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000617 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618 if not self.has_patchset:
619 patchset = RunGit(['config', self._PatchsetSetting()],
620 error_ok=True).strip()
621 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000622 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 else:
624 self.patchset = None
625 self.has_patchset = True
626 return self.patchset
627
628 def SetPatchset(self, patchset):
629 """Set this branch's patchset. If patchset=0, clears the patchset."""
630 if patchset:
631 RunGit(['config', self._PatchsetSetting(), str(patchset)])
632 else:
633 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000634 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 self.has_patchset = False
636
binji@chromium.org0281f522012-09-14 13:37:59 +0000637 def GetMostRecentPatchset(self, issue):
638 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000639 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000640
641 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000642 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000643 '/download/issue%s_%s.diff' % (issue, patchset))
644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 def SetIssue(self, issue):
646 """Set this branch's issue. If issue=0, clears the issue."""
647 if issue:
648 RunGit(['config', self._IssueSetting(), str(issue)])
649 if self.rietveld_server:
650 RunGit(['config', self._RietveldServer(), self.rietveld_server])
651 else:
652 RunGit(['config', '--unset', self._IssueSetting()])
653 self.SetPatchset(0)
654 self.has_issue = False
655
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000656 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000657 if not self.GitSanityChecks(upstream_branch):
658 DieWithError('\nGit sanity check failure')
659
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000660 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
661 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000662
663 # We use the sha1 of HEAD as a name of this change.
664 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000665 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000666 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000667 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000668 except subprocess2.CalledProcessError:
669 DieWithError(
670 ('\nFailed to diff against upstream branch %s!\n\n'
671 'This branch probably doesn\'t exist anymore. To reset the\n'
672 'tracking branch, please run\n'
673 ' git branch --set-upstream %s trunk\n'
674 'replacing trunk with origin/master or the relevant branch') %
675 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000676
maruel@chromium.org52424302012-08-29 15:14:30 +0000677 issue = self.GetIssue()
678 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000679 if issue:
680 description = self.GetDescription()
681 else:
682 # If the change was never uploaded, use the log messages of all commits
683 # up to the branch point, as git cl upload will prefill the description
684 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000685 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
686 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000687
688 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000689 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000690 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000691 name,
692 description,
693 absroot,
694 files,
695 issue,
696 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000697 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000698
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000699 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
700 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
701 change = self.GetChange(upstream_branch, author)
702
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000703 # Apply watchlists on upload.
704 if not committing:
705 watchlist = watchlists.Watchlists(change.RepositoryRoot())
706 files = [f.LocalPath() for f in change.AffectedFiles()]
707 self.SetWatchers(watchlist.GetWatchersForPaths(files))
708
709 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000710 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000711 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000712 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000713 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000714 except presubmit_support.PresubmitFailure, e:
715 DieWithError(
716 ('%s\nMaybe your depot_tools is out of date?\n'
717 'If all fails, contact maruel@') % e)
718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000720 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000721 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000722 self.RpcServer().update_description(issue, self.description)
723 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000725 def SetFlag(self, flag, value):
726 """Patchset must match."""
727 if not self.GetPatchset():
728 DieWithError('The patchset needs to match. Send another patchset.')
729 try:
730 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000731 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000732 except urllib2.HTTPError, e:
733 if e.code == 404:
734 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
735 if e.code == 403:
736 DieWithError(
737 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
738 'match?') % (self.GetIssue(), self.GetPatchset()))
739 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000741 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742 """Returns an upload.RpcServer() to access this review's rietveld instance.
743 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000744 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000745 self._rpc_server = rietveld.CachingRietveld(
746 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000747 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748
749 def _IssueSetting(self):
750 """Return the git setting that stores this change's issue."""
751 return 'branch.%s.rietveldissue' % self.GetBranch()
752
753 def _PatchsetSetting(self):
754 """Return the git setting that stores this change's most recent patchset."""
755 return 'branch.%s.rietveldpatchset' % self.GetBranch()
756
757 def _RietveldServer(self):
758 """Returns the git setting that stores this change's rietveld server."""
759 return 'branch.%s.rietveldserver' % self.GetBranch()
760
761
762def GetCodereviewSettingsInteractively():
763 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000764 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 server = settings.GetDefaultServerUrl(error_ok=True)
766 prompt = 'Rietveld server (host[:port])'
767 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000768 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769 if not server and not newserver:
770 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000771 if newserver:
772 newserver = gclient_utils.UpgradeToHttps(newserver)
773 if newserver != server:
774 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000776 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 prompt = caption
778 if initial:
779 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000780 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781 if new_val == 'x':
782 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000783 elif new_val:
784 if is_url:
785 new_val = gclient_utils.UpgradeToHttps(new_val)
786 if new_val != initial:
787 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000789 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000791 'tree-status-url', False)
792 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793
794 # TODO: configure a default branch to diff against, rather than this
795 # svn-based hackery.
796
797
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000798class ChangeDescription(object):
799 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000800 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000801 self.log_desc = log_desc
802 self.reviewers = reviewers
803 self.description = self.log_desc
804
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000805 def Prompt(self):
806 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000807# This will displayed on the codereview site.
808# The first line will also be used as the subject of the review.
809"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000810 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000811 if ('\nR=' not in self.description and
812 '\nTBR=' not in self.description and
813 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000814 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000815 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000816 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000817 content = content.rstrip('\n') + '\n'
818 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000819 if not content:
820 DieWithError('Running editor failed')
821 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000822 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000823 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000824 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000825
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000826 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000827 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000828 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000829 # Retrieves all reviewer lines
830 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000831 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000832 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000833 if reviewers:
834 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000835
836 def IsEmpty(self):
837 return not self.description
838
839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840def FindCodereviewSettingsFile(filename='codereview.settings'):
841 """Finds the given file starting in the cwd and going up.
842
843 Only looks up to the top of the repository unless an
844 'inherit-review-settings-ok' file exists in the root of the repository.
845 """
846 inherit_ok_file = 'inherit-review-settings-ok'
847 cwd = os.getcwd()
848 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
849 if os.path.isfile(os.path.join(root, inherit_ok_file)):
850 root = '/'
851 while True:
852 if filename in os.listdir(cwd):
853 if os.path.isfile(os.path.join(cwd, filename)):
854 return open(os.path.join(cwd, filename))
855 if cwd == root:
856 break
857 cwd = os.path.dirname(cwd)
858
859
860def LoadCodereviewSettingsFromFile(fileobj):
861 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000862 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864 def SetProperty(name, setting, unset_error_ok=False):
865 fullname = 'rietveld.' + name
866 if setting in keyvals:
867 RunGit(['config', fullname, keyvals[setting]])
868 else:
869 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
870
871 SetProperty('server', 'CODE_REVIEW_SERVER')
872 # Only server setting is required. Other settings can be absent.
873 # In that case, we ignore errors raised during option deletion attempt.
874 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
875 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
876 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
877
ukai@chromium.orge8077812012-02-03 03:41:46 +0000878 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
879 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
880 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
883 #should be of the form
884 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
885 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
886 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
887 keyvals['ORIGIN_URL_CONFIG']])
888
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000890def urlretrieve(source, destination):
891 """urllib is broken for SSL connections via a proxy therefore we
892 can't use urllib.urlretrieve()."""
893 with open(destination, 'w') as f:
894 f.write(urllib2.urlopen(source).read())
895
896
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000897def DownloadHooks(force):
898 """downloads hooks
899
900 Args:
901 force: True to update hooks. False to install hooks if not present.
902 """
903 if not settings.GetIsGerrit():
904 return
905 server_url = settings.GetDefaultServerUrl()
906 src = '%s/tools/hooks/commit-msg' % server_url
907 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
908 if not os.access(dst, os.X_OK):
909 if os.path.exists(dst):
910 if not force:
911 return
912 os.remove(dst)
913 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000914 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000915 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
916 except Exception:
917 if os.path.exists(dst):
918 os.remove(dst)
919 DieWithError('\nFailed to download hooks from %s' % src)
920
921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922@usage('[repo root containing codereview.settings]')
923def CMDconfig(parser, args):
924 """edit configuration for this tree"""
925
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000926 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 if len(args) == 0:
928 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000929 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 return 0
931
932 url = args[0]
933 if not url.endswith('codereview.settings'):
934 url = os.path.join(url, 'codereview.settings')
935
936 # Load code review settings and download hooks (if available).
937 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000938 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 return 0
940
941
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000942def CMDbaseurl(parser, args):
943 """get or set base-url for this branch"""
944 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
945 branch = ShortBranchName(branchref)
946 _, args = parser.parse_args(args)
947 if not args:
948 print("Current base-url:")
949 return RunGit(['config', 'branch.%s.base-url' % branch],
950 error_ok=False).strip()
951 else:
952 print("Setting base-url to %s" % args[0])
953 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
954 error_ok=False).strip()
955
956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957def CMDstatus(parser, args):
958 """show status of changelists"""
959 parser.add_option('--field',
960 help='print only specific field (desc|id|patch|url)')
961 (options, args) = parser.parse_args(args)
962
963 # TODO: maybe make show_branches a flag if necessary.
964 show_branches = not options.field
965
966 if show_branches:
967 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
968 if branches:
969 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000970 changes = (Changelist(branchref=b) for b in branches.splitlines())
971 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
972 alignment = max(5, max(len(b) for b in branches))
973 for branch in sorted(branches):
974 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975
976 cl = Changelist()
977 if options.field:
978 if options.field.startswith('desc'):
979 print cl.GetDescription()
980 elif options.field == 'id':
981 issueid = cl.GetIssue()
982 if issueid:
983 print issueid
984 elif options.field == 'patch':
985 patchset = cl.GetPatchset()
986 if patchset:
987 print patchset
988 elif options.field == 'url':
989 url = cl.GetIssueURL()
990 if url:
991 print url
992 else:
993 print
994 print 'Current branch:',
995 if not cl.GetIssue():
996 print 'no issue assigned.'
997 return 0
998 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000999 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 print 'Issue description:'
1001 print cl.GetDescription(pretty=True)
1002 return 0
1003
1004
1005@usage('[issue_number]')
1006def CMDissue(parser, args):
1007 """Set or display the current code review issue number.
1008
1009 Pass issue number 0 to clear the current issue.
1010"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001011 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012
1013 cl = Changelist()
1014 if len(args) > 0:
1015 try:
1016 issue = int(args[0])
1017 except ValueError:
1018 DieWithError('Pass a number to set the issue or none to list it.\n'
1019 'Maybe you want to run git cl status?')
1020 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001021 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022 return 0
1023
1024
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001025def CMDcomments(parser, args):
1026 """show review comments of the current changelist"""
1027 (_, args) = parser.parse_args(args)
1028 if args:
1029 parser.error('Unsupported argument: %s' % args)
1030
1031 cl = Changelist()
1032 if cl.GetIssue():
1033 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1034 for message in sorted(data['messages'], key=lambda x: x['date']):
1035 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1036 if message['text'].strip():
1037 print '\n'.join(' ' + l for l in message['text'].splitlines())
1038 return 0
1039
1040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041def CreateDescriptionFromLog(args):
1042 """Pulls out the commit log to use as a base for the CL description."""
1043 log_args = []
1044 if len(args) == 1 and not args[0].endswith('.'):
1045 log_args = [args[0] + '..']
1046 elif len(args) == 1 and args[0].endswith('...'):
1047 log_args = [args[0][:-1]]
1048 elif len(args) == 2:
1049 log_args = [args[0] + '..' + args[1]]
1050 else:
1051 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001052 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053
1054
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055def CMDpresubmit(parser, args):
1056 """run presubmit tests on the current changelist"""
1057 parser.add_option('--upload', action='store_true',
1058 help='Run upload hook instead of the push/dcommit hook')
sbc@chromium.org495ad152012-09-04 23:07:42 +00001059 parser.add_option('--force', action='store_true',
1060 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 (options, args) = parser.parse_args(args)
1062
ukai@chromium.org259e4682012-10-25 07:36:33 +00001063 if not options.force and is_dirty_git_tree('presubmit'):
1064 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065 return 1
1066
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001067 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 if args:
1069 base_branch = args[0]
1070 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001071 # Default to diffing against the common ancestor of the upstream branch.
1072 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001073
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001074 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001075 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001076 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001077 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078
1079
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001080def AddChangeIdToCommitMessage(options, args):
1081 """Re-commits using the current message, assumes the commit hook is in
1082 place.
1083 """
1084 log_desc = options.message or CreateDescriptionFromLog(args)
1085 git_command = ['commit', '--amend', '-m', log_desc]
1086 RunGit(git_command)
1087 new_log_desc = CreateDescriptionFromLog(args)
1088 if CHANGE_ID in new_log_desc:
1089 print 'git-cl: Added Change-Id to commit message.'
1090 else:
1091 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1092
1093
ukai@chromium.orge8077812012-02-03 03:41:46 +00001094def GerritUpload(options, args, cl):
1095 """upload the current branch to gerrit."""
1096 # We assume the remote called "origin" is the one we want.
1097 # It is probably not worthwhile to support different workflows.
1098 remote = 'origin'
1099 branch = 'master'
1100 if options.target_branch:
1101 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001103 log_desc = options.message or CreateDescriptionFromLog(args)
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001104 if CHANGE_ID not in log_desc:
1105 AddChangeIdToCommitMessage(options, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001106 if options.reviewers:
1107 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001108 change_desc = ChangeDescription(log_desc, options.reviewers)
1109 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +00001110 if change_desc.IsEmpty():
1111 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112 return 1
1113
ukai@chromium.orge8077812012-02-03 03:41:46 +00001114 receive_options = []
1115 cc = cl.GetCCList().split(',')
1116 if options.cc:
1117 cc += options.cc.split(',')
1118 cc = filter(None, cc)
1119 if cc:
1120 receive_options += ['--cc=' + email for email in cc]
1121 if change_desc.reviewers:
1122 reviewers = filter(None, change_desc.reviewers.split(','))
1123 if reviewers:
1124 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125
ukai@chromium.orge8077812012-02-03 03:41:46 +00001126 git_command = ['push']
1127 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001128 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001129 ' '.join(receive_options))
1130 git_command += [remote, 'HEAD:refs/for/' + branch]
1131 RunGit(git_command)
1132 # TODO(ukai): parse Change-Id: and set issue number?
1133 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001134
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135
ukai@chromium.orge8077812012-02-03 03:41:46 +00001136def RietveldUpload(options, args, cl):
1137 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1139 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 if options.emulate_svn_auto_props:
1141 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142
1143 change_desc = None
1144
1145 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001146 if options.title:
1147 upload_args.extend(['--title', options.title])
1148 elif options.message:
1149 # TODO(rogerta): for now, the -m option will also set the --title option
1150 # for upload.py. Soon this will be changed to set the --message option.
1151 # Will wait until people are used to typing -t instead of -m.
1152 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001153 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 print ("This branch is associated with issue %s. "
1155 "Adding patch to that issue." % cl.GetIssue())
1156 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001157 if options.title:
1158 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001159 message = options.message or CreateDescriptionFromLog(args)
1160 change_desc = ChangeDescription(message, options.reviewers)
1161 if not options.force:
1162 change_desc.Prompt()
1163 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001164
1165 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 print "Description is empty; aborting."
1167 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001168
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001169 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001170 if change_desc.reviewers:
1171 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001172 if options.send_mail:
1173 if not change_desc.reviewers:
1174 DieWithError("Must specify reviewers to send email.")
1175 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001176 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001177 if cc:
1178 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001180 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001181 if not options.find_copies:
1182 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 # Include the upstream repo's URL in the change -- this is useful for
1185 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001186 remote_url = cl.GetGitBaseUrlFromConfig()
1187 if not remote_url:
1188 if settings.GetIsGitSvn():
1189 # URL is dependent on the current directory.
1190 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1191 if data:
1192 keys = dict(line.split(': ', 1) for line in data.splitlines()
1193 if ': ' in line)
1194 remote_url = keys.get('URL', None)
1195 else:
1196 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1197 remote_url = (cl.GetRemoteUrl() + '@'
1198 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199 if remote_url:
1200 upload_args.extend(['--base_url', remote_url])
1201
1202 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001203 upload_args = ['upload'] + upload_args + args
1204 logging.info('upload.RealMain(%s)', upload_args)
1205 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001206 except KeyboardInterrupt:
1207 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 except:
1209 # If we got an exception after the user typed a description for their
1210 # change, back up the description before re-raising.
1211 if change_desc:
1212 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1213 print '\nGot exception while uploading -- saving description to %s\n' \
1214 % backup_path
1215 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001216 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 backup_file.close()
1218 raise
1219
1220 if not cl.GetIssue():
1221 cl.SetIssue(issue)
1222 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001223
1224 if options.use_commit_queue:
1225 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 return 0
1227
1228
ukai@chromium.orge8077812012-02-03 03:41:46 +00001229@usage('[args to "git diff"]')
1230def CMDupload(parser, args):
1231 """upload the current changelist to codereview"""
1232 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1233 help='bypass upload presubmit hook')
1234 parser.add_option('-f', action='store_true', dest='force',
1235 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001236 parser.add_option('-m', dest='message', help='message for patchset')
1237 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001238 parser.add_option('-r', '--reviewers',
1239 help='reviewer email addresses')
1240 parser.add_option('--cc',
1241 help='cc email addresses')
1242 parser.add_option('--send-mail', action='store_true',
1243 help='send email to reviewer immediately')
1244 parser.add_option("--emulate_svn_auto_props", action="store_true",
1245 dest="emulate_svn_auto_props",
1246 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001247 parser.add_option('-c', '--use-commit-queue', action='store_true',
1248 help='tell the commit queue to commit this patchset')
1249 if settings.GetIsGerrit():
1250 parser.add_option('--target_branch', dest='target_branch', default='master',
1251 help='target branch to upload')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001252 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001253 (options, args) = parser.parse_args(args)
1254
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001255 # Print warning if the user used the -m/--message argument. This will soon
1256 # change to -t/--title.
1257 if options.message:
1258 print >> sys.stderr, (
1259 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1260 'In the near future, -m or --message will send a message instead.\n'
1261 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001262
ukai@chromium.org259e4682012-10-25 07:36:33 +00001263 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001264 return 1
1265
1266 cl = Changelist()
1267 if args:
1268 # TODO(ukai): is it ok for gerrit case?
1269 base_branch = args[0]
1270 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001271 # Default to diffing against common ancestor of upstream branch
1272 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
1273 args = [base_branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001274
1275 if not options.bypass_hooks:
1276 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1277 may_prompt=not options.force,
1278 verbose=options.verbose,
1279 author=None)
1280 if not hook_results.should_continue():
1281 return 1
1282 if not options.reviewers and hook_results.reviewers:
1283 options.reviewers = hook_results.reviewers
1284
iannucci@chromium.org79540052012-10-19 23:15:26 +00001285 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001286 if settings.GetIsGerrit():
1287 return GerritUpload(options, args, cl)
1288 return RietveldUpload(options, args, cl)
1289
1290
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001291def IsSubmoduleMergeCommit(ref):
1292 # When submodules are added to the repo, we expect there to be a single
1293 # non-git-svn merge commit at remote HEAD with a signature comment.
1294 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001295 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001296 return RunGit(cmd) != ''
1297
1298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299def SendUpstream(parser, args, cmd):
1300 """Common code for CmdPush and CmdDCommit
1301
1302 Squashed commit into a single.
1303 Updates changelog with metadata (e.g. pointer to review).
1304 Pushes/dcommits the code upstream.
1305 Updates review and closes.
1306 """
1307 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1308 help='bypass upload presubmit hook')
1309 parser.add_option('-m', dest='message',
1310 help="override review description")
1311 parser.add_option('-f', action='store_true', dest='force',
1312 help="force yes to questions (don't prompt)")
1313 parser.add_option('-c', dest='contributor',
1314 help="external contributor for patch (appended to " +
1315 "description and used as author for git). Should be " +
1316 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001317 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 (options, args) = parser.parse_args(args)
1319 cl = Changelist()
1320
1321 if not args or cmd == 'push':
1322 # Default to merging against our best guess of the upstream branch.
1323 args = [cl.GetUpstreamBranch()]
1324
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001325 if options.contributor:
1326 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1327 print "Please provide contibutor as 'First Last <email@example.com>'"
1328 return 1
1329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001331 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
ukai@chromium.org259e4682012-10-25 07:36:33 +00001333 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 return 1
1335
1336 # This rev-list syntax means "show all commits not in my branch that
1337 # are in base_branch".
1338 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1339 base_branch]).splitlines()
1340 if upstream_commits:
1341 print ('Base branch "%s" has %d commits '
1342 'not in this branch.' % (base_branch, len(upstream_commits)))
1343 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1344 return 1
1345
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001346 # This is the revision `svn dcommit` will commit on top of.
1347 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1348 '--pretty=format:%H'])
1349
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001351 # If the base_head is a submodule merge commit, the first parent of the
1352 # base_head should be a git-svn commit, which is what we're interested in.
1353 base_svn_head = base_branch
1354 if base_has_submodules:
1355 base_svn_head += '^1'
1356
1357 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 if extra_commits:
1359 print ('This branch has %d additional commits not upstreamed yet.'
1360 % len(extra_commits.splitlines()))
1361 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1362 'before attempting to %s.' % (base_branch, cmd))
1363 return 1
1364
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001365 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001366 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001367 author = None
1368 if options.contributor:
1369 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001370 hook_results = cl.RunHook(
1371 committing=True,
1372 upstream_branch=base_branch,
1373 may_prompt=not options.force,
1374 verbose=options.verbose,
1375 author=author)
1376 if not hook_results.should_continue():
1377 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378
1379 if cmd == 'dcommit':
1380 # Check the tree status if the tree status URL is set.
1381 status = GetTreeStatus()
1382 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001383 print('The tree is closed. Please wait for it to reopen. Use '
1384 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385 return 1
1386 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001387 print('Unable to determine tree status. Please verify manually and '
1388 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001389 else:
1390 breakpad.SendStack(
1391 'GitClHooksBypassedCommit',
1392 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001393 (cl.GetRietveldServer(), cl.GetIssue()),
1394 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
1396 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001397 if not description and cl.GetIssue():
1398 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001400 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001401 if not cl.GetIssue() and options.bypass_hooks:
1402 description = CreateDescriptionFromLog([base_branch])
1403 else:
1404 print 'No description set.'
1405 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1406 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001408 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410
1411 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 description += "\nPatch from %s." % options.contributor
1413 print 'Description:', repr(description)
1414
1415 branches = [base_branch, cl.GetBranchRef()]
1416 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001417 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001418 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001420 # We want to squash all this branch's commits into one commit with the proper
1421 # description. We do this by doing a "reset --soft" to the base branch (which
1422 # keeps the working copy the same), then dcommitting that. If origin/master
1423 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1424 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001426 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1427 # Delete the branches if they exist.
1428 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1429 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1430 result = RunGitWithCode(showref_cmd)
1431 if result[0] == 0:
1432 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433
1434 # We might be in a directory that's present in this branch but not in the
1435 # trunk. Move up to the top of the tree so that git commands that expect a
1436 # valid CWD won't fail after we check out the merge branch.
1437 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1438 if rel_base_path:
1439 os.chdir(rel_base_path)
1440
1441 # Stuff our change into the merge branch.
1442 # We wrap in a try...finally block so if anything goes wrong,
1443 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001444 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001446 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1447 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448 if options.contributor:
1449 RunGit(['commit', '--author', options.contributor, '-m', description])
1450 else:
1451 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001452 if base_has_submodules:
1453 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1454 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1455 RunGit(['checkout', CHERRY_PICK_BRANCH])
1456 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 if cmd == 'push':
1458 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001459 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 retcode, output = RunGitWithCode(
1461 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1462 logging.debug(output)
1463 else:
1464 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001465 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001466 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001467 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 finally:
1469 # And then swap back to the original branch and clean up.
1470 RunGit(['checkout', '-q', cl.GetBranch()])
1471 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001472 if base_has_submodules:
1473 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474
1475 if cl.GetIssue():
1476 if cmd == 'dcommit' and 'Committed r' in output:
1477 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1478 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001479 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1480 for l in output.splitlines(False))
1481 match = filter(None, match)
1482 if len(match) != 1:
1483 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1484 output)
1485 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486 else:
1487 return 1
1488 viewvc_url = settings.GetViewVCUrl()
1489 if viewvc_url and revision:
1490 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001491 elif revision:
1492 cl.description += ('\n\nCommitted: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493 print ('Closing issue '
1494 '(you may be prompted for your codereview password)...')
1495 cl.CloseIssue()
1496 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001497
1498 if retcode == 0:
1499 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1500 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001501 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001502
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503 return 0
1504
1505
1506@usage('[upstream branch to apply against]')
1507def CMDdcommit(parser, args):
1508 """commit the current changelist via git-svn"""
1509 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001510 message = """This doesn't appear to be an SVN repository.
1511If your project has a git mirror with an upstream SVN master, you probably need
1512to run 'git svn init', see your project's git mirror documentation.
1513If your project has a true writeable upstream repository, you probably want
1514to run 'git cl push' instead.
1515Choose wisely, if you get this wrong, your commit might appear to succeed but
1516will instead be silently ignored."""
1517 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001518 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519 return SendUpstream(parser, args, 'dcommit')
1520
1521
1522@usage('[upstream branch to apply against]')
1523def CMDpush(parser, args):
1524 """commit the current changelist via git"""
1525 if settings.GetIsGitSvn():
1526 print('This appears to be an SVN repository.')
1527 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001528 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529 return SendUpstream(parser, args, 'push')
1530
1531
1532@usage('<patch url or issue id>')
1533def CMDpatch(parser, args):
1534 """patch in a code review"""
1535 parser.add_option('-b', dest='newbranch',
1536 help='create a new branch off trunk for the patch')
1537 parser.add_option('-f', action='store_true', dest='force',
1538 help='with -b, clobber any existing branch')
1539 parser.add_option('--reject', action='store_true', dest='reject',
1540 help='allow failed patches and spew .rej files')
1541 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1542 help="don't commit after patch applies")
1543 (options, args) = parser.parse_args(args)
1544 if len(args) != 1:
1545 parser.print_help()
1546 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001547 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001549 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001550 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001551
maruel@chromium.org52424302012-08-29 15:14:30 +00001552 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001553 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001554 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001555 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001556 patchset = cl.GetMostRecentPatchset(issue)
1557 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001558 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001559 # Assume it's a URL to the patch. Default to https.
1560 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001561 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001562 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 DieWithError('Must pass an issue ID or full URL for '
1564 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001565 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001566 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001567 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001568
1569 if options.newbranch:
1570 if options.force:
1571 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001572 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001573 RunGit(['checkout', '-b', options.newbranch,
1574 Changelist().GetUpstreamBranch()])
1575
1576 # Switch up to the top-level directory, if necessary, in preparation for
1577 # applying the patch.
1578 top = RunGit(['rev-parse', '--show-cdup']).strip()
1579 if top:
1580 os.chdir(top)
1581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582 # Git patches have a/ at the beginning of source paths. We strip that out
1583 # with a sed script rather than the -p flag to patch so we can feed either
1584 # Git or svn-style patches into the same apply command.
1585 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001586 try:
1587 patch_data = subprocess2.check_output(
1588 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1589 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001590 DieWithError('Git patch mungling failed.')
1591 logging.info(patch_data)
1592 # We use "git apply" to apply the patch instead of "patch" so that we can
1593 # pick up file adds.
1594 # The --index flag means: also insert into the index (so we catch adds).
1595 cmd = ['git', 'apply', '--index', '-p0']
1596 if options.reject:
1597 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001598 try:
1599 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1600 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001601 DieWithError('Failed to apply the patch')
1602
1603 # If we had an issue, commit the current state and register the issue.
1604 if not options.nocommit:
1605 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1606 cl = Changelist()
1607 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001608 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 print "Committed patch."
1610 else:
1611 print "Patch applied to index."
1612 return 0
1613
1614
1615def CMDrebase(parser, args):
1616 """rebase current branch on top of svn repo"""
1617 # Provide a wrapper for git svn rebase to help avoid accidental
1618 # git svn dcommit.
1619 # It's the only command that doesn't use parser at all since we just defer
1620 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001621 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622
1623
1624def GetTreeStatus():
1625 """Fetches the tree status and returns either 'open', 'closed',
1626 'unknown' or 'unset'."""
1627 url = settings.GetTreeStatusUrl(error_ok=True)
1628 if url:
1629 status = urllib2.urlopen(url).read().lower()
1630 if status.find('closed') != -1 or status == '0':
1631 return 'closed'
1632 elif status.find('open') != -1 or status == '1':
1633 return 'open'
1634 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001635 return 'unset'
1636
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001638def GetTreeStatusReason():
1639 """Fetches the tree status from a json url and returns the message
1640 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001641 url = settings.GetTreeStatusUrl()
1642 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643 connection = urllib2.urlopen(json_url)
1644 status = json.loads(connection.read())
1645 connection.close()
1646 return status['message']
1647
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649def CMDtree(parser, args):
1650 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001651 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001652 status = GetTreeStatus()
1653 if 'unset' == status:
1654 print 'You must configure your tree status URL by running "git cl config".'
1655 return 2
1656
1657 print "The tree is %s" % status
1658 print
1659 print GetTreeStatusReason()
1660 if status != 'open':
1661 return 1
1662 return 0
1663
1664
maruel@chromium.org15192402012-09-06 12:38:29 +00001665def CMDtry(parser, args):
1666 """Triggers a try job through Rietveld."""
1667 group = optparse.OptionGroup(parser, "Try job options")
1668 group.add_option(
1669 "-b", "--bot", action="append",
1670 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1671 "times to specify multiple builders. ex: "
1672 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1673 "the try server waterfall for the builders name and the tests "
1674 "available. Can also be used to specify gtest_filter, e.g. "
1675 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1676 group.add_option(
1677 "-r", "--revision",
1678 help="Revision to use for the try job; default: the "
1679 "revision will be determined by the try server; see "
1680 "its waterfall for more info")
1681 group.add_option(
1682 "-c", "--clobber", action="store_true", default=False,
1683 help="Force a clobber before building; e.g. don't do an "
1684 "incremental build")
1685 group.add_option(
1686 "--project",
1687 help="Override which project to use. Projects are defined "
1688 "server-side to define what default bot set to use")
1689 group.add_option(
1690 "-t", "--testfilter", action="append", default=[],
1691 help=("Apply a testfilter to all the selected builders. Unless the "
1692 "builders configurations are similar, use multiple "
1693 "--bot <builder>:<test> arguments."))
1694 group.add_option(
1695 "-n", "--name", help="Try job name; default to current branch name")
1696 parser.add_option_group(group)
1697 options, args = parser.parse_args(args)
1698
1699 if args:
1700 parser.error('Unknown arguments: %s' % args)
1701
1702 cl = Changelist()
1703 if not cl.GetIssue():
1704 parser.error('Need to upload first')
1705
1706 if not options.name:
1707 options.name = cl.GetBranch()
1708
1709 # Process --bot and --testfilter.
1710 if not options.bot:
1711 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001712 change = cl.GetChange(
1713 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1714 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001715 options.bot = presubmit_support.DoGetTrySlaves(
1716 change,
1717 change.LocalPaths(),
1718 settings.GetRoot(),
1719 None,
1720 None,
1721 options.verbose,
1722 sys.stdout)
1723 if not options.bot:
1724 parser.error('No default try builder to try, use --bot')
1725
1726 builders_and_tests = {}
1727 for bot in options.bot:
1728 if ':' in bot:
1729 builder, tests = bot.split(':', 1)
1730 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1731 elif ',' in bot:
1732 parser.error('Specify one bot per --bot flag')
1733 else:
1734 builders_and_tests.setdefault(bot, []).append('defaulttests')
1735
1736 if options.testfilter:
1737 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1738 builders_and_tests = dict(
1739 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1740 if t != ['compile'])
1741
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001742 if any('triggered' in b for b in builders_and_tests):
1743 print >> sys.stderr, (
1744 'ERROR You are trying to send a job to a triggered bot. This type of'
1745 ' bot requires an\ninitial job from a parent (usually a builder). '
1746 'Instead send your job to the parent.\n'
1747 'Bot list: %s' % builders_and_tests)
1748 return 1
1749
maruel@chromium.org15192402012-09-06 12:38:29 +00001750 patchset = cl.GetPatchset()
1751 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001752 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001753
1754 cl.RpcServer().trigger_try_jobs(
1755 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1756 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001757 print('Tried jobs on:')
1758 length = max(len(builder) for builder in builders_and_tests)
1759 for builder in sorted(builders_and_tests):
1760 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001761 return 0
1762
1763
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001764@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001766 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001767 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001768 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001769 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001770 return 0
1771
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001773 if args:
1774 # One arg means set upstream branch.
1775 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1776 cl = Changelist()
1777 print "Upstream branch set to " + cl.GetUpstreamBranch()
1778 else:
1779 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780 return 0
1781
1782
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001783def CMDset_commit(parser, args):
1784 """set the commit bit"""
1785 _, args = parser.parse_args(args)
1786 if args:
1787 parser.error('Unrecognized args: %s' % ' '.join(args))
1788 cl = Changelist()
1789 cl.SetFlag('commit', '1')
1790 return 0
1791
1792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001793def Command(name):
1794 return getattr(sys.modules[__name__], 'CMD' + name, None)
1795
1796
1797def CMDhelp(parser, args):
1798 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001799 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800 if len(args) == 1:
1801 return main(args + ['--help'])
1802 parser.print_help()
1803 return 0
1804
1805
1806def GenUsage(parser, command):
1807 """Modify an OptParse object with the function's documentation."""
1808 obj = Command(command)
1809 more = getattr(obj, 'usage_more', '')
1810 if command == 'help':
1811 command = '<command>'
1812 else:
1813 # OptParser.description prefer nicely non-formatted strings.
1814 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1815 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1816
1817
1818def main(argv):
1819 """Doesn't parse the arguments here, just find the right subcommand to
1820 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001821 if sys.hexversion < 0x02060000:
1822 print >> sys.stderr, (
1823 '\nYour python version %s is unsupported, please upgrade.\n' %
1824 sys.version.split(' ', 1)[0])
1825 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001826 # Reload settings.
1827 global settings
1828 settings = Settings()
1829
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830 # Do it late so all commands are listed.
1831 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1832 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1833 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1834
1835 # Create the option parse and add --verbose support.
1836 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001837 parser.add_option(
1838 '-v', '--verbose', action='count', default=0,
1839 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 old_parser_args = parser.parse_args
1841 def Parse(args):
1842 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001843 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001845 elif options.verbose:
1846 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847 else:
1848 logging.basicConfig(level=logging.WARNING)
1849 return options, args
1850 parser.parse_args = Parse
1851
1852 if argv:
1853 command = Command(argv[0])
1854 if command:
1855 # "fix" the usage and the description now that we know the subcommand.
1856 GenUsage(parser, argv[0])
1857 try:
1858 return command(parser, argv[1:])
1859 except urllib2.HTTPError, e:
1860 if e.code != 500:
1861 raise
1862 DieWithError(
1863 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1864 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1865
1866 # Not a known command. Default to help.
1867 GenUsage(parser, 'help')
1868 return CMDhelp(parser, argv)
1869
1870
1871if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001872 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001873 sys.exit(main(sys.argv[1:]))