blob: c374a10cf692a8f60f4b76c5f1a40499fed0aa98 [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:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000352 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000353 return self.viewvc_url
354
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000355 def GetDefaultCCList(self):
356 return self._GetConfig('rietveld.cc', error_ok=True)
357
ukai@chromium.orge8077812012-02-03 03:41:46 +0000358 def GetIsGerrit(self):
359 """Return true if this repo is assosiated with gerrit code review system."""
360 if self.is_gerrit is None:
361 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
362 return self.is_gerrit
363
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000364 def _GetConfig(self, param, **kwargs):
365 self.LazyUpdateIfNeeded()
366 return RunGit(['config', param], **kwargs).strip()
367
368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000369def ShortBranchName(branch):
370 """Convert a name like 'refs/heads/foo' to just 'foo'."""
371 return branch.replace('refs/heads/', '')
372
373
374class Changelist(object):
375 def __init__(self, branchref=None):
376 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000377 global settings
378 if not settings:
379 # Happens when git_cl.py is used as a utility library.
380 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000381 settings.GetDefaultServerUrl()
382 self.branchref = branchref
383 if self.branchref:
384 self.branch = ShortBranchName(self.branchref)
385 else:
386 self.branch = None
387 self.rietveld_server = None
388 self.upstream_branch = None
389 self.has_issue = False
390 self.issue = None
391 self.has_description = False
392 self.description = None
393 self.has_patchset = False
394 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000395 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000396 self.cc = None
397 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000398 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000399
400 def GetCCList(self):
401 """Return the users cc'd on this CL.
402
403 Return is a string suitable for passing to gcl with the --cc flag.
404 """
405 if self.cc is None:
406 base_cc = settings .GetDefaultCCList()
407 more_cc = ','.join(self.watchers)
408 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
409 return self.cc
410
411 def SetWatchers(self, watchers):
412 """Set the list of email addresses that should be cc'd based on the changed
413 files in this CL.
414 """
415 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000416
417 def GetBranch(self):
418 """Returns the short branch name, e.g. 'master'."""
419 if not self.branch:
420 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
421 self.branch = ShortBranchName(self.branchref)
422 return self.branch
423
424 def GetBranchRef(self):
425 """Returns the full branch name, e.g. 'refs/heads/master'."""
426 self.GetBranch() # Poke the lazy loader.
427 return self.branchref
428
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000429 @staticmethod
430 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431 """Returns a tuple containg remote and remote ref,
432 e.g. 'origin', 'refs/heads/master'
433 """
434 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000435 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
436 error_ok=True).strip()
437 if upstream_branch:
438 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
439 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000440 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
441 error_ok=True).strip()
442 if upstream_branch:
443 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000444 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000445 # Fall back on trying a git-svn upstream branch.
446 if settings.GetIsGitSvn():
447 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000448 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000449 # Else, try to guess the origin remote.
450 remote_branches = RunGit(['branch', '-r']).split()
451 if 'origin/master' in remote_branches:
452 # Fall back on origin/master if it exits.
453 remote = 'origin'
454 upstream_branch = 'refs/heads/master'
455 elif 'origin/trunk' in remote_branches:
456 # Fall back on origin/trunk if it exists. Generally a shared
457 # git-svn clone
458 remote = 'origin'
459 upstream_branch = 'refs/heads/trunk'
460 else:
461 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000462Either pass complete "git diff"-style arguments, like
463 git cl upload origin/master
464or verify this branch is set up to track another (via the --track argument to
465"git checkout -b ...").""")
466
467 return remote, upstream_branch
468
469 def GetUpstreamBranch(self):
470 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000471 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472 if remote is not '.':
473 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
474 self.upstream_branch = upstream_branch
475 return self.upstream_branch
476
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000477 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000478 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000479 remote, branch = None, self.GetBranch()
480 seen_branches = set()
481 while branch not in seen_branches:
482 seen_branches.add(branch)
483 remote, branch = self.FetchUpstreamTuple(branch)
484 branch = ShortBranchName(branch)
485 if remote != '.' or branch.startswith('refs/remotes'):
486 break
487 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000488 remotes = RunGit(['remote'], error_ok=True).split()
489 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000490 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000491 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000492 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000493 logging.warning('Could not determine which remote this change is '
494 'associated with, so defaulting to "%s". This may '
495 'not be what you want. You may prevent this message '
496 'by running "git svn info" as documented here: %s',
497 self._remote,
498 GIT_INSTRUCTIONS_URL)
499 else:
500 logging.warn('Could not determine which remote this change is '
501 'associated with. You may prevent this message by '
502 'running "git svn info" as documented here: %s',
503 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000504 branch = 'HEAD'
505 if branch.startswith('refs/remotes'):
506 self._remote = (remote, branch)
507 else:
508 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000509 return self._remote
510
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000511 def GitSanityChecks(self, upstream_git_obj):
512 """Checks git repo status and ensures diff is from local commits."""
513
514 # Verify the commit we're diffing against is in our current branch.
515 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
516 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
517 if upstream_sha != common_ancestor:
518 print >> sys.stderr, (
519 'ERROR: %s is not in the current branch. You may need to rebase '
520 'your tracking branch' % upstream_sha)
521 return False
522
523 # List the commits inside the diff, and verify they are all local.
524 commits_in_diff = RunGit(
525 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
526 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
527 remote_branch = remote_branch.strip()
528 if code != 0:
529 _, remote_branch = self.GetRemoteBranch()
530
531 commits_in_remote = RunGit(
532 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
533
534 common_commits = set(commits_in_diff) & set(commits_in_remote)
535 if common_commits:
536 print >> sys.stderr, (
537 'ERROR: Your diff contains %d commits already in %s.\n'
538 'Run "git log --oneline %s..HEAD" to get a list of commits in '
539 'the diff. If you are using a custom git flow, you can override'
540 ' the reference used for this check with "git config '
541 'gitcl.remotebranch <git-ref>".' % (
542 len(common_commits), remote_branch, upstream_git_obj))
543 return False
544 return True
545
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000546 def GetGitBaseUrlFromConfig(self):
547 """Return the configured base URL from branch.<branchname>.baseurl.
548
549 Returns None if it is not set.
550 """
551 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
552 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000553
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000554 def GetRemoteUrl(self):
555 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
556
557 Returns None if there is no remote.
558 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000559 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000560 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
561
562 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000563 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000564 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000565 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
566 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000567 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000568 else:
569 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 self.has_issue = True
571 return self.issue
572
573 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000574 if not self.rietveld_server:
575 # If we're on a branch then get the server potentially associated
576 # with that branch.
577 if self.GetIssue():
578 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
579 ['config', self._RietveldServer()], error_ok=True).strip())
580 if not self.rietveld_server:
581 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000582 return self.rietveld_server
583
584 def GetIssueURL(self):
585 """Get the URL for a particular issue."""
586 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
587
588 def GetDescription(self, pretty=False):
589 if not self.has_description:
590 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000591 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000592 try:
593 self.description = self.RpcServer().get_description(issue).strip()
594 except urllib2.HTTPError, e:
595 if e.code == 404:
596 DieWithError(
597 ('\nWhile fetching the description for issue %d, received a '
598 '404 (not found)\n'
599 'error. It is likely that you deleted this '
600 'issue on the server. If this is the\n'
601 'case, please run\n\n'
602 ' git cl issue 0\n\n'
603 'to clear the association with the deleted issue. Then run '
604 'this command again.') % issue)
605 else:
606 DieWithError(
607 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 self.has_description = True
609 if pretty:
610 wrapper = textwrap.TextWrapper()
611 wrapper.initial_indent = wrapper.subsequent_indent = ' '
612 return wrapper.fill(self.description)
613 return self.description
614
615 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000616 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617 if not self.has_patchset:
618 patchset = RunGit(['config', self._PatchsetSetting()],
619 error_ok=True).strip()
620 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000621 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 else:
623 self.patchset = None
624 self.has_patchset = True
625 return self.patchset
626
627 def SetPatchset(self, patchset):
628 """Set this branch's patchset. If patchset=0, clears the patchset."""
629 if patchset:
630 RunGit(['config', self._PatchsetSetting(), str(patchset)])
631 else:
632 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000633 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 self.has_patchset = False
635
binji@chromium.org0281f522012-09-14 13:37:59 +0000636 def GetMostRecentPatchset(self, issue):
637 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000638 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000639
640 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000641 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000642 '/download/issue%s_%s.diff' % (issue, patchset))
643
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644 def SetIssue(self, issue):
645 """Set this branch's issue. If issue=0, clears the issue."""
646 if issue:
647 RunGit(['config', self._IssueSetting(), str(issue)])
648 if self.rietveld_server:
649 RunGit(['config', self._RietveldServer(), self.rietveld_server])
650 else:
651 RunGit(['config', '--unset', self._IssueSetting()])
652 self.SetPatchset(0)
653 self.has_issue = False
654
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000655 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000656 if not self.GitSanityChecks(upstream_branch):
657 DieWithError('\nGit sanity check failure')
658
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000659 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
660 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000661
662 # We use the sha1 of HEAD as a name of this change.
663 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000664 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000665 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000666 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000667 except subprocess2.CalledProcessError:
668 DieWithError(
669 ('\nFailed to diff against upstream branch %s!\n\n'
670 'This branch probably doesn\'t exist anymore. To reset the\n'
671 'tracking branch, please run\n'
672 ' git branch --set-upstream %s trunk\n'
673 'replacing trunk with origin/master or the relevant branch') %
674 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000675
maruel@chromium.org52424302012-08-29 15:14:30 +0000676 issue = self.GetIssue()
677 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000678 if issue:
679 description = self.GetDescription()
680 else:
681 # If the change was never uploaded, use the log messages of all commits
682 # up to the branch point, as git cl upload will prefill the description
683 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000684 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
685 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000686
687 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000688 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000689 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000690 name,
691 description,
692 absroot,
693 files,
694 issue,
695 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000696 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000697
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000698 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
699 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
700 change = self.GetChange(upstream_branch, author)
701
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000702 # Apply watchlists on upload.
703 if not committing:
704 watchlist = watchlists.Watchlists(change.RepositoryRoot())
705 files = [f.LocalPath() for f in change.AffectedFiles()]
706 self.SetWatchers(watchlist.GetWatchersForPaths(files))
707
708 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000709 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000710 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000711 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000712 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000713 except presubmit_support.PresubmitFailure, e:
714 DieWithError(
715 ('%s\nMaybe your depot_tools is out of date?\n'
716 'If all fails, contact maruel@') % e)
717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000719 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000720 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000721 self.RpcServer().update_description(issue, self.description)
722 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000724 def SetFlag(self, flag, value):
725 """Patchset must match."""
726 if not self.GetPatchset():
727 DieWithError('The patchset needs to match. Send another patchset.')
728 try:
729 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000730 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000731 except urllib2.HTTPError, e:
732 if e.code == 404:
733 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
734 if e.code == 403:
735 DieWithError(
736 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
737 'match?') % (self.GetIssue(), self.GetPatchset()))
738 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000740 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 """Returns an upload.RpcServer() to access this review's rietveld instance.
742 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000743 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000744 self._rpc_server = rietveld.CachingRietveld(
745 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000746 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747
748 def _IssueSetting(self):
749 """Return the git setting that stores this change's issue."""
750 return 'branch.%s.rietveldissue' % self.GetBranch()
751
752 def _PatchsetSetting(self):
753 """Return the git setting that stores this change's most recent patchset."""
754 return 'branch.%s.rietveldpatchset' % self.GetBranch()
755
756 def _RietveldServer(self):
757 """Returns the git setting that stores this change's rietveld server."""
758 return 'branch.%s.rietveldserver' % self.GetBranch()
759
760
761def GetCodereviewSettingsInteractively():
762 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000763 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 server = settings.GetDefaultServerUrl(error_ok=True)
765 prompt = 'Rietveld server (host[:port])'
766 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000767 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 if not server and not newserver:
769 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000770 if newserver:
771 newserver = gclient_utils.UpgradeToHttps(newserver)
772 if newserver != server:
773 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000775 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 prompt = caption
777 if initial:
778 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000779 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780 if new_val == 'x':
781 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000782 elif new_val:
783 if is_url:
784 new_val = gclient_utils.UpgradeToHttps(new_val)
785 if new_val != initial:
786 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000788 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000790 'tree-status-url', False)
791 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792
793 # TODO: configure a default branch to diff against, rather than this
794 # svn-based hackery.
795
796
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000797class ChangeDescription(object):
798 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000799 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000800 self.log_desc = log_desc
801 self.reviewers = reviewers
802 self.description = self.log_desc
803
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000804 def Prompt(self):
805 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000806# This will displayed on the codereview site.
807# The first line will also be used as the subject of the review.
808"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000809 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000810 if ('\nR=' not in self.description and
811 '\nTBR=' not in self.description and
812 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000813 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000814 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000815 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000816 content = content.rstrip('\n') + '\n'
817 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000818 if not content:
819 DieWithError('Running editor failed')
820 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000821 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000822 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000823 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000824
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000825 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000826 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000827 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000828 # Retrieves all reviewer lines
829 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000830 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000831 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000832 if reviewers:
833 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000834
835 def IsEmpty(self):
836 return not self.description
837
838
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839def FindCodereviewSettingsFile(filename='codereview.settings'):
840 """Finds the given file starting in the cwd and going up.
841
842 Only looks up to the top of the repository unless an
843 'inherit-review-settings-ok' file exists in the root of the repository.
844 """
845 inherit_ok_file = 'inherit-review-settings-ok'
846 cwd = os.getcwd()
847 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
848 if os.path.isfile(os.path.join(root, inherit_ok_file)):
849 root = '/'
850 while True:
851 if filename in os.listdir(cwd):
852 if os.path.isfile(os.path.join(cwd, filename)):
853 return open(os.path.join(cwd, filename))
854 if cwd == root:
855 break
856 cwd = os.path.dirname(cwd)
857
858
859def LoadCodereviewSettingsFromFile(fileobj):
860 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000861 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000862
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 def SetProperty(name, setting, unset_error_ok=False):
864 fullname = 'rietveld.' + name
865 if setting in keyvals:
866 RunGit(['config', fullname, keyvals[setting]])
867 else:
868 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
869
870 SetProperty('server', 'CODE_REVIEW_SERVER')
871 # Only server setting is required. Other settings can be absent.
872 # In that case, we ignore errors raised during option deletion attempt.
873 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
874 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
875 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
876
ukai@chromium.orge8077812012-02-03 03:41:46 +0000877 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
878 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
879 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
882 #should be of the form
883 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
884 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
885 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
886 keyvals['ORIGIN_URL_CONFIG']])
887
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000889def urlretrieve(source, destination):
890 """urllib is broken for SSL connections via a proxy therefore we
891 can't use urllib.urlretrieve()."""
892 with open(destination, 'w') as f:
893 f.write(urllib2.urlopen(source).read())
894
895
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000896def DownloadHooks(force):
897 """downloads hooks
898
899 Args:
900 force: True to update hooks. False to install hooks if not present.
901 """
902 if not settings.GetIsGerrit():
903 return
904 server_url = settings.GetDefaultServerUrl()
905 src = '%s/tools/hooks/commit-msg' % server_url
906 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
907 if not os.access(dst, os.X_OK):
908 if os.path.exists(dst):
909 if not force:
910 return
911 os.remove(dst)
912 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000913 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000914 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
915 except Exception:
916 if os.path.exists(dst):
917 os.remove(dst)
918 DieWithError('\nFailed to download hooks from %s' % src)
919
920
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921@usage('[repo root containing codereview.settings]')
922def CMDconfig(parser, args):
923 """edit configuration for this tree"""
924
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000925 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 if len(args) == 0:
927 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000928 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 return 0
930
931 url = args[0]
932 if not url.endswith('codereview.settings'):
933 url = os.path.join(url, 'codereview.settings')
934
935 # Load code review settings and download hooks (if available).
936 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000937 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 return 0
939
940
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000941def CMDbaseurl(parser, args):
942 """get or set base-url for this branch"""
943 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
944 branch = ShortBranchName(branchref)
945 _, args = parser.parse_args(args)
946 if not args:
947 print("Current base-url:")
948 return RunGit(['config', 'branch.%s.base-url' % branch],
949 error_ok=False).strip()
950 else:
951 print("Setting base-url to %s" % args[0])
952 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
953 error_ok=False).strip()
954
955
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956def CMDstatus(parser, args):
957 """show status of changelists"""
958 parser.add_option('--field',
959 help='print only specific field (desc|id|patch|url)')
960 (options, args) = parser.parse_args(args)
961
962 # TODO: maybe make show_branches a flag if necessary.
963 show_branches = not options.field
964
965 if show_branches:
966 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
967 if branches:
968 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000969 changes = (Changelist(branchref=b) for b in branches.splitlines())
970 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
971 alignment = max(5, max(len(b) for b in branches))
972 for branch in sorted(branches):
973 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974
975 cl = Changelist()
976 if options.field:
977 if options.field.startswith('desc'):
978 print cl.GetDescription()
979 elif options.field == 'id':
980 issueid = cl.GetIssue()
981 if issueid:
982 print issueid
983 elif options.field == 'patch':
984 patchset = cl.GetPatchset()
985 if patchset:
986 print patchset
987 elif options.field == 'url':
988 url = cl.GetIssueURL()
989 if url:
990 print url
991 else:
992 print
993 print 'Current branch:',
994 if not cl.GetIssue():
995 print 'no issue assigned.'
996 return 0
997 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000998 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 print 'Issue description:'
1000 print cl.GetDescription(pretty=True)
1001 return 0
1002
1003
1004@usage('[issue_number]')
1005def CMDissue(parser, args):
1006 """Set or display the current code review issue number.
1007
1008 Pass issue number 0 to clear the current issue.
1009"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001010 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011
1012 cl = Changelist()
1013 if len(args) > 0:
1014 try:
1015 issue = int(args[0])
1016 except ValueError:
1017 DieWithError('Pass a number to set the issue or none to list it.\n'
1018 'Maybe you want to run git cl status?')
1019 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001020 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021 return 0
1022
1023
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001024def CMDcomments(parser, args):
1025 """show review comments of the current changelist"""
1026 (_, args) = parser.parse_args(args)
1027 if args:
1028 parser.error('Unsupported argument: %s' % args)
1029
1030 cl = Changelist()
1031 if cl.GetIssue():
1032 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1033 for message in sorted(data['messages'], key=lambda x: x['date']):
1034 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1035 if message['text'].strip():
1036 print '\n'.join(' ' + l for l in message['text'].splitlines())
1037 return 0
1038
1039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040def CreateDescriptionFromLog(args):
1041 """Pulls out the commit log to use as a base for the CL description."""
1042 log_args = []
1043 if len(args) == 1 and not args[0].endswith('.'):
1044 log_args = [args[0] + '..']
1045 elif len(args) == 1 and args[0].endswith('...'):
1046 log_args = [args[0][:-1]]
1047 elif len(args) == 2:
1048 log_args = [args[0] + '..' + args[1]]
1049 else:
1050 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001051 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052
1053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054def CMDpresubmit(parser, args):
1055 """run presubmit tests on the current changelist"""
1056 parser.add_option('--upload', action='store_true',
1057 help='Run upload hook instead of the push/dcommit hook')
sbc@chromium.org495ad152012-09-04 23:07:42 +00001058 parser.add_option('--force', action='store_true',
1059 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001060 (options, args) = parser.parse_args(args)
1061
ukai@chromium.org259e4682012-10-25 07:36:33 +00001062 if not options.force and is_dirty_git_tree('presubmit'):
1063 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 return 1
1065
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001066 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067 if args:
1068 base_branch = args[0]
1069 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001070 # Default to diffing against the common ancestor of the upstream branch.
1071 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001073 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001074 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001075 author=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])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001158 message = options.message or CreateDescriptionFromLog(args)
1159 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')
1248 if settings.GetIsGerrit():
1249 parser.add_option('--target_branch', dest='target_branch', default='master',
1250 help='target branch to upload')
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
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001254 # Print warning if the user used the -m/--message argument. This will soon
1255 # change to -t/--title.
1256 if options.message:
1257 print >> sys.stderr, (
1258 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1259 'In the near future, -m or --message will send a message instead.\n'
1260 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001261
ukai@chromium.org259e4682012-10-25 07:36:33 +00001262 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001263 return 1
1264
1265 cl = Changelist()
1266 if args:
1267 # TODO(ukai): is it ok for gerrit case?
1268 base_branch = args[0]
1269 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 # Default to diffing against common ancestor of upstream branch
1271 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
1272 args = [base_branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001273
1274 if not options.bypass_hooks:
1275 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1276 may_prompt=not options.force,
1277 verbose=options.verbose,
1278 author=None)
1279 if not hook_results.should_continue():
1280 return 1
1281 if not options.reviewers and hook_results.reviewers:
1282 options.reviewers = hook_results.reviewers
1283
iannucci@chromium.org79540052012-10-19 23:15:26 +00001284 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001285 if settings.GetIsGerrit():
1286 return GerritUpload(options, args, cl)
1287 return RietveldUpload(options, args, cl)
1288
1289
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001290def IsSubmoduleMergeCommit(ref):
1291 # When submodules are added to the repo, we expect there to be a single
1292 # non-git-svn merge commit at remote HEAD with a signature comment.
1293 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001294 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001295 return RunGit(cmd) != ''
1296
1297
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298def SendUpstream(parser, args, cmd):
1299 """Common code for CmdPush and CmdDCommit
1300
1301 Squashed commit into a single.
1302 Updates changelog with metadata (e.g. pointer to review).
1303 Pushes/dcommits the code upstream.
1304 Updates review and closes.
1305 """
1306 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1307 help='bypass upload presubmit hook')
1308 parser.add_option('-m', dest='message',
1309 help="override review description")
1310 parser.add_option('-f', action='store_true', dest='force',
1311 help="force yes to questions (don't prompt)")
1312 parser.add_option('-c', dest='contributor',
1313 help="external contributor for patch (appended to " +
1314 "description and used as author for git). Should be " +
1315 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001316 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 (options, args) = parser.parse_args(args)
1318 cl = Changelist()
1319
1320 if not args or cmd == 'push':
1321 # Default to merging against our best guess of the upstream branch.
1322 args = [cl.GetUpstreamBranch()]
1323
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001324 if options.contributor:
1325 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1326 print "Please provide contibutor as 'First Last <email@example.com>'"
1327 return 1
1328
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001330 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331
ukai@chromium.org259e4682012-10-25 07:36:33 +00001332 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333 return 1
1334
1335 # This rev-list syntax means "show all commits not in my branch that
1336 # are in base_branch".
1337 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1338 base_branch]).splitlines()
1339 if upstream_commits:
1340 print ('Base branch "%s" has %d commits '
1341 'not in this branch.' % (base_branch, len(upstream_commits)))
1342 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1343 return 1
1344
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001345 # This is the revision `svn dcommit` will commit on top of.
1346 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1347 '--pretty=format:%H'])
1348
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001350 # If the base_head is a submodule merge commit, the first parent of the
1351 # base_head should be a git-svn commit, which is what we're interested in.
1352 base_svn_head = base_branch
1353 if base_has_submodules:
1354 base_svn_head += '^1'
1355
1356 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 if extra_commits:
1358 print ('This branch has %d additional commits not upstreamed yet.'
1359 % len(extra_commits.splitlines()))
1360 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1361 'before attempting to %s.' % (base_branch, cmd))
1362 return 1
1363
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001364 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001365 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001366 author = None
1367 if options.contributor:
1368 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001369 hook_results = cl.RunHook(
1370 committing=True,
1371 upstream_branch=base_branch,
1372 may_prompt=not options.force,
1373 verbose=options.verbose,
1374 author=author)
1375 if not hook_results.should_continue():
1376 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377
1378 if cmd == 'dcommit':
1379 # Check the tree status if the tree status URL is set.
1380 status = GetTreeStatus()
1381 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001382 print('The tree is closed. Please wait for it to reopen. Use '
1383 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384 return 1
1385 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001386 print('Unable to determine tree status. Please verify manually and '
1387 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001388 else:
1389 breakpad.SendStack(
1390 'GitClHooksBypassedCommit',
1391 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001392 (cl.GetRietveldServer(), cl.GetIssue()),
1393 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394
1395 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001396 if not description and cl.GetIssue():
1397 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001399 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001400 if not cl.GetIssue() and options.bypass_hooks:
1401 description = CreateDescriptionFromLog([base_branch])
1402 else:
1403 print 'No description set.'
1404 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1405 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001407 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
1410 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 description += "\nPatch from %s." % options.contributor
1412 print 'Description:', repr(description)
1413
1414 branches = [base_branch, cl.GetBranchRef()]
1415 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001416 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001417 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001419 # We want to squash all this branch's commits into one commit with the proper
1420 # description. We do this by doing a "reset --soft" to the base branch (which
1421 # keeps the working copy the same), then dcommitting that. If origin/master
1422 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1423 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001424 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001425 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1426 # Delete the branches if they exist.
1427 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1428 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1429 result = RunGitWithCode(showref_cmd)
1430 if result[0] == 0:
1431 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432
1433 # We might be in a directory that's present in this branch but not in the
1434 # trunk. Move up to the top of the tree so that git commands that expect a
1435 # valid CWD won't fail after we check out the merge branch.
1436 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1437 if rel_base_path:
1438 os.chdir(rel_base_path)
1439
1440 # Stuff our change into the merge branch.
1441 # We wrap in a try...finally block so if anything goes wrong,
1442 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001443 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001445 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1446 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 if options.contributor:
1448 RunGit(['commit', '--author', options.contributor, '-m', description])
1449 else:
1450 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001451 if base_has_submodules:
1452 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1453 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1454 RunGit(['checkout', CHERRY_PICK_BRANCH])
1455 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 if cmd == 'push':
1457 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001458 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 retcode, output = RunGitWithCode(
1460 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1461 logging.debug(output)
1462 else:
1463 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001464 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001465 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001466 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 finally:
1468 # And then swap back to the original branch and clean up.
1469 RunGit(['checkout', '-q', cl.GetBranch()])
1470 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001471 if base_has_submodules:
1472 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473
1474 if cl.GetIssue():
1475 if cmd == 'dcommit' and 'Committed r' in output:
1476 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1477 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001478 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1479 for l in output.splitlines(False))
1480 match = filter(None, match)
1481 if len(match) != 1:
1482 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1483 output)
1484 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485 else:
1486 return 1
1487 viewvc_url = settings.GetViewVCUrl()
1488 if viewvc_url and revision:
1489 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001490 elif revision:
1491 cl.description += ('\n\nCommitted: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 print ('Closing issue '
1493 '(you may be prompted for your codereview password)...')
1494 cl.CloseIssue()
1495 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001496
1497 if retcode == 0:
1498 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1499 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001500 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001501
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502 return 0
1503
1504
1505@usage('[upstream branch to apply against]')
1506def CMDdcommit(parser, args):
1507 """commit the current changelist via git-svn"""
1508 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001509 message = """This doesn't appear to be an SVN repository.
1510If your project has a git mirror with an upstream SVN master, you probably need
1511to run 'git svn init', see your project's git mirror documentation.
1512If your project has a true writeable upstream repository, you probably want
1513to run 'git cl push' instead.
1514Choose wisely, if you get this wrong, your commit might appear to succeed but
1515will instead be silently ignored."""
1516 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001517 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 return SendUpstream(parser, args, 'dcommit')
1519
1520
1521@usage('[upstream branch to apply against]')
1522def CMDpush(parser, args):
1523 """commit the current changelist via git"""
1524 if settings.GetIsGitSvn():
1525 print('This appears to be an SVN repository.')
1526 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001527 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001528 return SendUpstream(parser, args, 'push')
1529
1530
1531@usage('<patch url or issue id>')
1532def CMDpatch(parser, args):
1533 """patch in a code review"""
1534 parser.add_option('-b', dest='newbranch',
1535 help='create a new branch off trunk for the patch')
1536 parser.add_option('-f', action='store_true', dest='force',
1537 help='with -b, clobber any existing branch')
1538 parser.add_option('--reject', action='store_true', dest='reject',
1539 help='allow failed patches and spew .rej files')
1540 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1541 help="don't commit after patch applies")
1542 (options, args) = parser.parse_args(args)
1543 if len(args) != 1:
1544 parser.print_help()
1545 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001546 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001548 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001549 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001550
maruel@chromium.org52424302012-08-29 15:14:30 +00001551 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001553 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001554 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001555 patchset = cl.GetMostRecentPatchset(issue)
1556 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001558 # Assume it's a URL to the patch. Default to https.
1559 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001560 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001561 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562 DieWithError('Must pass an issue ID or full URL for '
1563 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001564 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001565 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001566 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567
1568 if options.newbranch:
1569 if options.force:
1570 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001571 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572 RunGit(['checkout', '-b', options.newbranch,
1573 Changelist().GetUpstreamBranch()])
1574
1575 # Switch up to the top-level directory, if necessary, in preparation for
1576 # applying the patch.
1577 top = RunGit(['rev-parse', '--show-cdup']).strip()
1578 if top:
1579 os.chdir(top)
1580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001581 # Git patches have a/ at the beginning of source paths. We strip that out
1582 # with a sed script rather than the -p flag to patch so we can feed either
1583 # Git or svn-style patches into the same apply command.
1584 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001585 try:
1586 patch_data = subprocess2.check_output(
1587 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1588 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589 DieWithError('Git patch mungling failed.')
1590 logging.info(patch_data)
1591 # We use "git apply" to apply the patch instead of "patch" so that we can
1592 # pick up file adds.
1593 # The --index flag means: also insert into the index (so we catch adds).
1594 cmd = ['git', 'apply', '--index', '-p0']
1595 if options.reject:
1596 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001597 try:
1598 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1599 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600 DieWithError('Failed to apply the patch')
1601
1602 # If we had an issue, commit the current state and register the issue.
1603 if not options.nocommit:
1604 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1605 cl = Changelist()
1606 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001607 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001608 print "Committed patch."
1609 else:
1610 print "Patch applied to index."
1611 return 0
1612
1613
1614def CMDrebase(parser, args):
1615 """rebase current branch on top of svn repo"""
1616 # Provide a wrapper for git svn rebase to help avoid accidental
1617 # git svn dcommit.
1618 # It's the only command that doesn't use parser at all since we just defer
1619 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001620 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621
1622
1623def GetTreeStatus():
1624 """Fetches the tree status and returns either 'open', 'closed',
1625 'unknown' or 'unset'."""
1626 url = settings.GetTreeStatusUrl(error_ok=True)
1627 if url:
1628 status = urllib2.urlopen(url).read().lower()
1629 if status.find('closed') != -1 or status == '0':
1630 return 'closed'
1631 elif status.find('open') != -1 or status == '1':
1632 return 'open'
1633 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001634 return 'unset'
1635
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001636
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001637def GetTreeStatusReason():
1638 """Fetches the tree status from a json url and returns the message
1639 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001640 url = settings.GetTreeStatusUrl()
1641 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 connection = urllib2.urlopen(json_url)
1643 status = json.loads(connection.read())
1644 connection.close()
1645 return status['message']
1646
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001647
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001648def CMDtree(parser, args):
1649 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001650 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 status = GetTreeStatus()
1652 if 'unset' == status:
1653 print 'You must configure your tree status URL by running "git cl config".'
1654 return 2
1655
1656 print "The tree is %s" % status
1657 print
1658 print GetTreeStatusReason()
1659 if status != 'open':
1660 return 1
1661 return 0
1662
1663
maruel@chromium.org15192402012-09-06 12:38:29 +00001664def CMDtry(parser, args):
1665 """Triggers a try job through Rietveld."""
1666 group = optparse.OptionGroup(parser, "Try job options")
1667 group.add_option(
1668 "-b", "--bot", action="append",
1669 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1670 "times to specify multiple builders. ex: "
1671 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1672 "the try server waterfall for the builders name and the tests "
1673 "available. Can also be used to specify gtest_filter, e.g. "
1674 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1675 group.add_option(
1676 "-r", "--revision",
1677 help="Revision to use for the try job; default: the "
1678 "revision will be determined by the try server; see "
1679 "its waterfall for more info")
1680 group.add_option(
1681 "-c", "--clobber", action="store_true", default=False,
1682 help="Force a clobber before building; e.g. don't do an "
1683 "incremental build")
1684 group.add_option(
1685 "--project",
1686 help="Override which project to use. Projects are defined "
1687 "server-side to define what default bot set to use")
1688 group.add_option(
1689 "-t", "--testfilter", action="append", default=[],
1690 help=("Apply a testfilter to all the selected builders. Unless the "
1691 "builders configurations are similar, use multiple "
1692 "--bot <builder>:<test> arguments."))
1693 group.add_option(
1694 "-n", "--name", help="Try job name; default to current branch name")
1695 parser.add_option_group(group)
1696 options, args = parser.parse_args(args)
1697
1698 if args:
1699 parser.error('Unknown arguments: %s' % args)
1700
1701 cl = Changelist()
1702 if not cl.GetIssue():
1703 parser.error('Need to upload first')
1704
1705 if not options.name:
1706 options.name = cl.GetBranch()
1707
1708 # Process --bot and --testfilter.
1709 if not options.bot:
1710 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001711 change = cl.GetChange(
1712 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1713 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001714 options.bot = presubmit_support.DoGetTrySlaves(
1715 change,
1716 change.LocalPaths(),
1717 settings.GetRoot(),
1718 None,
1719 None,
1720 options.verbose,
1721 sys.stdout)
1722 if not options.bot:
1723 parser.error('No default try builder to try, use --bot')
1724
1725 builders_and_tests = {}
1726 for bot in options.bot:
1727 if ':' in bot:
1728 builder, tests = bot.split(':', 1)
1729 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1730 elif ',' in bot:
1731 parser.error('Specify one bot per --bot flag')
1732 else:
1733 builders_and_tests.setdefault(bot, []).append('defaulttests')
1734
1735 if options.testfilter:
1736 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1737 builders_and_tests = dict(
1738 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1739 if t != ['compile'])
1740
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001741 if any('triggered' in b for b in builders_and_tests):
1742 print >> sys.stderr, (
1743 'ERROR You are trying to send a job to a triggered bot. This type of'
1744 ' bot requires an\ninitial job from a parent (usually a builder). '
1745 'Instead send your job to the parent.\n'
1746 'Bot list: %s' % builders_and_tests)
1747 return 1
1748
maruel@chromium.org15192402012-09-06 12:38:29 +00001749 patchset = cl.GetPatchset()
1750 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001751 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001752
1753 cl.RpcServer().trigger_try_jobs(
1754 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1755 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001756 print('Tried jobs on:')
1757 length = max(len(builder) for builder in builders_and_tests)
1758 for builder in sorted(builders_and_tests):
1759 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001760 return 0
1761
1762
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001763@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001765 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001766 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001767 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001768 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001769 return 0
1770
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001772 if args:
1773 # One arg means set upstream branch.
1774 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1775 cl = Changelist()
1776 print "Upstream branch set to " + cl.GetUpstreamBranch()
1777 else:
1778 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 return 0
1780
1781
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001782def CMDset_commit(parser, args):
1783 """set the commit bit"""
1784 _, args = parser.parse_args(args)
1785 if args:
1786 parser.error('Unrecognized args: %s' % ' '.join(args))
1787 cl = Changelist()
1788 cl.SetFlag('commit', '1')
1789 return 0
1790
1791
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001792def Command(name):
1793 return getattr(sys.modules[__name__], 'CMD' + name, None)
1794
1795
1796def CMDhelp(parser, args):
1797 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001798 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001799 if len(args) == 1:
1800 return main(args + ['--help'])
1801 parser.print_help()
1802 return 0
1803
1804
1805def GenUsage(parser, command):
1806 """Modify an OptParse object with the function's documentation."""
1807 obj = Command(command)
1808 more = getattr(obj, 'usage_more', '')
1809 if command == 'help':
1810 command = '<command>'
1811 else:
1812 # OptParser.description prefer nicely non-formatted strings.
1813 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1814 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1815
1816
1817def main(argv):
1818 """Doesn't parse the arguments here, just find the right subcommand to
1819 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001820 if sys.hexversion < 0x02060000:
1821 print >> sys.stderr, (
1822 '\nYour python version %s is unsupported, please upgrade.\n' %
1823 sys.version.split(' ', 1)[0])
1824 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001825 # Reload settings.
1826 global settings
1827 settings = Settings()
1828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 # Do it late so all commands are listed.
1830 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1831 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1832 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1833
1834 # Create the option parse and add --verbose support.
1835 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001836 parser.add_option(
1837 '-v', '--verbose', action='count', default=0,
1838 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839 old_parser_args = parser.parse_args
1840 def Parse(args):
1841 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001842 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001844 elif options.verbose:
1845 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 else:
1847 logging.basicConfig(level=logging.WARNING)
1848 return options, args
1849 parser.parse_args = Parse
1850
1851 if argv:
1852 command = Command(argv[0])
1853 if command:
1854 # "fix" the usage and the description now that we know the subcommand.
1855 GenUsage(parser, argv[0])
1856 try:
1857 return command(parser, argv[1:])
1858 except urllib2.HTTPError, e:
1859 if e.code != 500:
1860 raise
1861 DieWithError(
1862 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1863 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1864
1865 # Not a known command. Default to help.
1866 GenUsage(parser, 'help')
1867 return CMDhelp(parser, argv)
1868
1869
1870if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001871 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872 sys.exit(main(sys.argv[1:]))