blob: 2fd5d852f15d892dc6ae968fbd2ceee33cdb4666 [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
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000698 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000699 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000700
701 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000702 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000703 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000704 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000705 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000706 except presubmit_support.PresubmitFailure, e:
707 DieWithError(
708 ('%s\nMaybe your depot_tools is out of date?\n'
709 'If all fails, contact maruel@') % e)
710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000712 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000713 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000714 self.RpcServer().update_description(issue, self.description)
715 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000717 def SetFlag(self, flag, value):
718 """Patchset must match."""
719 if not self.GetPatchset():
720 DieWithError('The patchset needs to match. Send another patchset.')
721 try:
722 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000723 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000724 except urllib2.HTTPError, e:
725 if e.code == 404:
726 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
727 if e.code == 403:
728 DieWithError(
729 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
730 'match?') % (self.GetIssue(), self.GetPatchset()))
731 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000733 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 """Returns an upload.RpcServer() to access this review's rietveld instance.
735 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000736 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000737 self._rpc_server = rietveld.CachingRietveld(
738 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000739 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740
741 def _IssueSetting(self):
742 """Return the git setting that stores this change's issue."""
743 return 'branch.%s.rietveldissue' % self.GetBranch()
744
745 def _PatchsetSetting(self):
746 """Return the git setting that stores this change's most recent patchset."""
747 return 'branch.%s.rietveldpatchset' % self.GetBranch()
748
749 def _RietveldServer(self):
750 """Returns the git setting that stores this change's rietveld server."""
751 return 'branch.%s.rietveldserver' % self.GetBranch()
752
753
754def GetCodereviewSettingsInteractively():
755 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000756 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757 server = settings.GetDefaultServerUrl(error_ok=True)
758 prompt = 'Rietveld server (host[:port])'
759 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000760 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761 if not server and not newserver:
762 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000763 if newserver:
764 newserver = gclient_utils.UpgradeToHttps(newserver)
765 if newserver != server:
766 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000768 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769 prompt = caption
770 if initial:
771 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000772 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773 if new_val == 'x':
774 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000775 elif new_val:
776 if is_url:
777 new_val = gclient_utils.UpgradeToHttps(new_val)
778 if new_val != initial:
779 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000781 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000783 'tree-status-url', False)
784 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785
786 # TODO: configure a default branch to diff against, rather than this
787 # svn-based hackery.
788
789
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000790class ChangeDescription(object):
791 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000792 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000793 self.log_desc = log_desc
794 self.reviewers = reviewers
795 self.description = self.log_desc
796
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000797 def Prompt(self):
798 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000799# This will displayed on the codereview site.
800# The first line will also be used as the subject of the review.
801"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000802 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000803 if ('\nR=' not in self.description and
804 '\nTBR=' not in self.description and
805 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000806 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000807 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000808 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000809 content = content.rstrip('\n') + '\n'
810 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000811 if not content:
812 DieWithError('Running editor failed')
813 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000814 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000815 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000816 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000817
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000818 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000819 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000820 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000821 # Retrieves all reviewer lines
822 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000823 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000824 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000825 if reviewers:
826 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000827
828 def IsEmpty(self):
829 return not self.description
830
831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832def FindCodereviewSettingsFile(filename='codereview.settings'):
833 """Finds the given file starting in the cwd and going up.
834
835 Only looks up to the top of the repository unless an
836 'inherit-review-settings-ok' file exists in the root of the repository.
837 """
838 inherit_ok_file = 'inherit-review-settings-ok'
839 cwd = os.getcwd()
840 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
841 if os.path.isfile(os.path.join(root, inherit_ok_file)):
842 root = '/'
843 while True:
844 if filename in os.listdir(cwd):
845 if os.path.isfile(os.path.join(cwd, filename)):
846 return open(os.path.join(cwd, filename))
847 if cwd == root:
848 break
849 cwd = os.path.dirname(cwd)
850
851
852def LoadCodereviewSettingsFromFile(fileobj):
853 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000854 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000856 def SetProperty(name, setting, unset_error_ok=False):
857 fullname = 'rietveld.' + name
858 if setting in keyvals:
859 RunGit(['config', fullname, keyvals[setting]])
860 else:
861 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
862
863 SetProperty('server', 'CODE_REVIEW_SERVER')
864 # Only server setting is required. Other settings can be absent.
865 # In that case, we ignore errors raised during option deletion attempt.
866 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
867 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
868 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
869
ukai@chromium.orge8077812012-02-03 03:41:46 +0000870 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
871 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
872 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000873
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
875 #should be of the form
876 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
877 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
878 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
879 keyvals['ORIGIN_URL_CONFIG']])
880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000882def urlretrieve(source, destination):
883 """urllib is broken for SSL connections via a proxy therefore we
884 can't use urllib.urlretrieve()."""
885 with open(destination, 'w') as f:
886 f.write(urllib2.urlopen(source).read())
887
888
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000889def DownloadHooks(force):
890 """downloads hooks
891
892 Args:
893 force: True to update hooks. False to install hooks if not present.
894 """
895 if not settings.GetIsGerrit():
896 return
897 server_url = settings.GetDefaultServerUrl()
898 src = '%s/tools/hooks/commit-msg' % server_url
899 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
900 if not os.access(dst, os.X_OK):
901 if os.path.exists(dst):
902 if not force:
903 return
904 os.remove(dst)
905 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000906 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000907 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
908 except Exception:
909 if os.path.exists(dst):
910 os.remove(dst)
911 DieWithError('\nFailed to download hooks from %s' % src)
912
913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914@usage('[repo root containing codereview.settings]')
915def CMDconfig(parser, args):
916 """edit configuration for this tree"""
917
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000918 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 if len(args) == 0:
920 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000921 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 return 0
923
924 url = args[0]
925 if not url.endswith('codereview.settings'):
926 url = os.path.join(url, 'codereview.settings')
927
928 # Load code review settings and download hooks (if available).
929 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000930 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931 return 0
932
933
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000934def CMDbaseurl(parser, args):
935 """get or set base-url for this branch"""
936 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
937 branch = ShortBranchName(branchref)
938 _, args = parser.parse_args(args)
939 if not args:
940 print("Current base-url:")
941 return RunGit(['config', 'branch.%s.base-url' % branch],
942 error_ok=False).strip()
943 else:
944 print("Setting base-url to %s" % args[0])
945 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
946 error_ok=False).strip()
947
948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000949def CMDstatus(parser, args):
950 """show status of changelists"""
951 parser.add_option('--field',
952 help='print only specific field (desc|id|patch|url)')
953 (options, args) = parser.parse_args(args)
954
955 # TODO: maybe make show_branches a flag if necessary.
956 show_branches = not options.field
957
958 if show_branches:
959 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
960 if branches:
961 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000962 changes = (Changelist(branchref=b) for b in branches.splitlines())
963 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
964 alignment = max(5, max(len(b) for b in branches))
965 for branch in sorted(branches):
966 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967
968 cl = Changelist()
969 if options.field:
970 if options.field.startswith('desc'):
971 print cl.GetDescription()
972 elif options.field == 'id':
973 issueid = cl.GetIssue()
974 if issueid:
975 print issueid
976 elif options.field == 'patch':
977 patchset = cl.GetPatchset()
978 if patchset:
979 print patchset
980 elif options.field == 'url':
981 url = cl.GetIssueURL()
982 if url:
983 print url
984 else:
985 print
986 print 'Current branch:',
987 if not cl.GetIssue():
988 print 'no issue assigned.'
989 return 0
990 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000991 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992 print 'Issue description:'
993 print cl.GetDescription(pretty=True)
994 return 0
995
996
997@usage('[issue_number]')
998def CMDissue(parser, args):
999 """Set or display the current code review issue number.
1000
1001 Pass issue number 0 to clear the current issue.
1002"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001003 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004
1005 cl = Changelist()
1006 if len(args) > 0:
1007 try:
1008 issue = int(args[0])
1009 except ValueError:
1010 DieWithError('Pass a number to set the issue or none to list it.\n'
1011 'Maybe you want to run git cl status?')
1012 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001013 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 return 0
1015
1016
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001017def CMDcomments(parser, args):
1018 """show review comments of the current changelist"""
1019 (_, args) = parser.parse_args(args)
1020 if args:
1021 parser.error('Unsupported argument: %s' % args)
1022
1023 cl = Changelist()
1024 if cl.GetIssue():
1025 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1026 for message in sorted(data['messages'], key=lambda x: x['date']):
1027 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1028 if message['text'].strip():
1029 print '\n'.join(' ' + l for l in message['text'].splitlines())
1030 return 0
1031
1032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033def CreateDescriptionFromLog(args):
1034 """Pulls out the commit log to use as a base for the CL description."""
1035 log_args = []
1036 if len(args) == 1 and not args[0].endswith('.'):
1037 log_args = [args[0] + '..']
1038 elif len(args) == 1 and args[0].endswith('...'):
1039 log_args = [args[0][:-1]]
1040 elif len(args) == 2:
1041 log_args = [args[0] + '..' + args[1]]
1042 else:
1043 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001044 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045
1046
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047def CMDpresubmit(parser, args):
1048 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001049 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001051 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001052 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 (options, args) = parser.parse_args(args)
1054
ukai@chromium.org259e4682012-10-25 07:36:33 +00001055 if not options.force and is_dirty_git_tree('presubmit'):
1056 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 return 1
1058
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001059 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001060 if args:
1061 base_branch = args[0]
1062 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001063 # Default to diffing against the common ancestor of the upstream branch.
1064 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001066 cl.RunHook(
1067 committing=not options.upload,
1068 may_prompt=False,
1069 verbose=options.verbose,
1070 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001071 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072
1073
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001074def AddChangeIdToCommitMessage(options, args):
1075 """Re-commits using the current message, assumes the commit hook is in
1076 place.
1077 """
1078 log_desc = options.message or CreateDescriptionFromLog(args)
1079 git_command = ['commit', '--amend', '-m', log_desc]
1080 RunGit(git_command)
1081 new_log_desc = CreateDescriptionFromLog(args)
1082 if CHANGE_ID in new_log_desc:
1083 print 'git-cl: Added Change-Id to commit message.'
1084 else:
1085 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1086
1087
ukai@chromium.orge8077812012-02-03 03:41:46 +00001088def GerritUpload(options, args, cl):
1089 """upload the current branch to gerrit."""
1090 # We assume the remote called "origin" is the one we want.
1091 # It is probably not worthwhile to support different workflows.
1092 remote = 'origin'
1093 branch = 'master'
1094 if options.target_branch:
1095 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001097 log_desc = options.message or CreateDescriptionFromLog(args)
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001098 if CHANGE_ID not in log_desc:
1099 AddChangeIdToCommitMessage(options, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001100 if options.reviewers:
1101 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001102 change_desc = ChangeDescription(log_desc, options.reviewers)
1103 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +00001104 if change_desc.IsEmpty():
1105 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 return 1
1107
ukai@chromium.orge8077812012-02-03 03:41:46 +00001108 receive_options = []
1109 cc = cl.GetCCList().split(',')
1110 if options.cc:
1111 cc += options.cc.split(',')
1112 cc = filter(None, cc)
1113 if cc:
1114 receive_options += ['--cc=' + email for email in cc]
1115 if change_desc.reviewers:
1116 reviewers = filter(None, change_desc.reviewers.split(','))
1117 if reviewers:
1118 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119
ukai@chromium.orge8077812012-02-03 03:41:46 +00001120 git_command = ['push']
1121 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001122 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001123 ' '.join(receive_options))
1124 git_command += [remote, 'HEAD:refs/for/' + branch]
1125 RunGit(git_command)
1126 # TODO(ukai): parse Change-Id: and set issue number?
1127 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
ukai@chromium.orge8077812012-02-03 03:41:46 +00001130def RietveldUpload(options, args, cl):
1131 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1133 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 if options.emulate_svn_auto_props:
1135 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136
1137 change_desc = None
1138
1139 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001140 if options.title:
1141 upload_args.extend(['--title', options.title])
1142 elif options.message:
1143 # TODO(rogerta): for now, the -m option will also set the --title option
1144 # for upload.py. Soon this will be changed to set the --message option.
1145 # Will wait until people are used to typing -t instead of -m.
1146 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001147 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 print ("This branch is associated with issue %s. "
1149 "Adding patch to that issue." % cl.GetIssue())
1150 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001151 if options.title:
1152 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001153 message = options.message or CreateDescriptionFromLog(args)
1154 change_desc = ChangeDescription(message, options.reviewers)
1155 if not options.force:
1156 change_desc.Prompt()
1157 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001158
1159 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 print "Description is empty; aborting."
1161 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001162
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001163 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001164 if change_desc.reviewers:
1165 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001166 if options.send_mail:
1167 if not change_desc.reviewers:
1168 DieWithError("Must specify reviewers to send email.")
1169 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001170 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001171 if cc:
1172 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001174 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001175 if not options.find_copies:
1176 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 # Include the upstream repo's URL in the change -- this is useful for
1179 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001180 remote_url = cl.GetGitBaseUrlFromConfig()
1181 if not remote_url:
1182 if settings.GetIsGitSvn():
1183 # URL is dependent on the current directory.
1184 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1185 if data:
1186 keys = dict(line.split(': ', 1) for line in data.splitlines()
1187 if ': ' in line)
1188 remote_url = keys.get('URL', None)
1189 else:
1190 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1191 remote_url = (cl.GetRemoteUrl() + '@'
1192 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 if remote_url:
1194 upload_args.extend(['--base_url', remote_url])
1195
1196 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001197 upload_args = ['upload'] + upload_args + args
1198 logging.info('upload.RealMain(%s)', upload_args)
1199 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001200 except KeyboardInterrupt:
1201 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 except:
1203 # If we got an exception after the user typed a description for their
1204 # change, back up the description before re-raising.
1205 if change_desc:
1206 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1207 print '\nGot exception while uploading -- saving description to %s\n' \
1208 % backup_path
1209 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001210 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 backup_file.close()
1212 raise
1213
1214 if not cl.GetIssue():
1215 cl.SetIssue(issue)
1216 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001217
1218 if options.use_commit_queue:
1219 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 return 0
1221
1222
ukai@chromium.orge8077812012-02-03 03:41:46 +00001223@usage('[args to "git diff"]')
1224def CMDupload(parser, args):
1225 """upload the current changelist to codereview"""
1226 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1227 help='bypass upload presubmit hook')
1228 parser.add_option('-f', action='store_true', dest='force',
1229 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001230 parser.add_option('-m', dest='message', help='message for patchset')
1231 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001232 parser.add_option('-r', '--reviewers',
1233 help='reviewer email addresses')
1234 parser.add_option('--cc',
1235 help='cc email addresses')
1236 parser.add_option('--send-mail', action='store_true',
1237 help='send email to reviewer immediately')
1238 parser.add_option("--emulate_svn_auto_props", action="store_true",
1239 dest="emulate_svn_auto_props",
1240 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001241 parser.add_option('-c', '--use-commit-queue', action='store_true',
1242 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001243 parser.add_option('--target_branch',
1244 help='When uploading to gerrit, remote branch to '
1245 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001246 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001247 (options, args) = parser.parse_args(args)
1248
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001249 if options.target_branch and not settings.GetIsGerrit():
1250 parser.error('Use --target_branch for non gerrit repository.')
1251
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001252 # Print warning if the user used the -m/--message argument. This will soon
1253 # change to -t/--title.
1254 if options.message:
1255 print >> sys.stderr, (
1256 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1257 'In the near future, -m or --message will send a message instead.\n'
1258 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001259
ukai@chromium.org259e4682012-10-25 07:36:33 +00001260 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001261 return 1
1262
1263 cl = Changelist()
1264 if args:
1265 # TODO(ukai): is it ok for gerrit case?
1266 base_branch = args[0]
1267 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001268 # Default to diffing against common ancestor of upstream branch
1269 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001270 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001271
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001272 # Apply watchlists on upload.
1273 change = cl.GetChange(base_branch, None)
1274 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1275 files = [f.LocalPath() for f in change.AffectedFiles()]
1276 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1277
ukai@chromium.orge8077812012-02-03 03:41:46 +00001278 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001279 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001280 may_prompt=not options.force,
1281 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001282 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001283 if not hook_results.should_continue():
1284 return 1
1285 if not options.reviewers and hook_results.reviewers:
1286 options.reviewers = hook_results.reviewers
1287
iannucci@chromium.org79540052012-10-19 23:15:26 +00001288 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001289 if settings.GetIsGerrit():
1290 return GerritUpload(options, args, cl)
1291 return RietveldUpload(options, args, cl)
1292
1293
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001294def IsSubmoduleMergeCommit(ref):
1295 # When submodules are added to the repo, we expect there to be a single
1296 # non-git-svn merge commit at remote HEAD with a signature comment.
1297 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001298 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001299 return RunGit(cmd) != ''
1300
1301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302def SendUpstream(parser, args, cmd):
1303 """Common code for CmdPush and CmdDCommit
1304
1305 Squashed commit into a single.
1306 Updates changelog with metadata (e.g. pointer to review).
1307 Pushes/dcommits the code upstream.
1308 Updates review and closes.
1309 """
1310 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1311 help='bypass upload presubmit hook')
1312 parser.add_option('-m', dest='message',
1313 help="override review description")
1314 parser.add_option('-f', action='store_true', dest='force',
1315 help="force yes to questions (don't prompt)")
1316 parser.add_option('-c', dest='contributor',
1317 help="external contributor for patch (appended to " +
1318 "description and used as author for git). Should be " +
1319 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001320 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 (options, args) = parser.parse_args(args)
1322 cl = Changelist()
1323
1324 if not args or cmd == 'push':
1325 # Default to merging against our best guess of the upstream branch.
1326 args = [cl.GetUpstreamBranch()]
1327
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001328 if options.contributor:
1329 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1330 print "Please provide contibutor as 'First Last <email@example.com>'"
1331 return 1
1332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001334 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335
ukai@chromium.org259e4682012-10-25 07:36:33 +00001336 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 return 1
1338
1339 # This rev-list syntax means "show all commits not in my branch that
1340 # are in base_branch".
1341 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1342 base_branch]).splitlines()
1343 if upstream_commits:
1344 print ('Base branch "%s" has %d commits '
1345 'not in this branch.' % (base_branch, len(upstream_commits)))
1346 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1347 return 1
1348
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001349 # This is the revision `svn dcommit` will commit on top of.
1350 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1351 '--pretty=format:%H'])
1352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001354 # If the base_head is a submodule merge commit, the first parent of the
1355 # base_head should be a git-svn commit, which is what we're interested in.
1356 base_svn_head = base_branch
1357 if base_has_submodules:
1358 base_svn_head += '^1'
1359
1360 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361 if extra_commits:
1362 print ('This branch has %d additional commits not upstreamed yet.'
1363 % len(extra_commits.splitlines()))
1364 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1365 'before attempting to %s.' % (base_branch, cmd))
1366 return 1
1367
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001368 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001369 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001370 author = None
1371 if options.contributor:
1372 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001373 hook_results = cl.RunHook(
1374 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001375 may_prompt=not options.force,
1376 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001377 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001378 if not hook_results.should_continue():
1379 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380
1381 if cmd == 'dcommit':
1382 # Check the tree status if the tree status URL is set.
1383 status = GetTreeStatus()
1384 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001385 print('The tree is closed. Please wait for it to reopen. Use '
1386 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 return 1
1388 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001389 print('Unable to determine tree status. Please verify manually and '
1390 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001391 else:
1392 breakpad.SendStack(
1393 'GitClHooksBypassedCommit',
1394 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001395 (cl.GetRietveldServer(), cl.GetIssue()),
1396 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397
1398 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001399 if not description and cl.GetIssue():
1400 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001402 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001403 if not cl.GetIssue() and options.bypass_hooks:
1404 description = CreateDescriptionFromLog([base_branch])
1405 else:
1406 print 'No description set.'
1407 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1408 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001410 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412
1413 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 description += "\nPatch from %s." % options.contributor
1415 print 'Description:', repr(description)
1416
1417 branches = [base_branch, cl.GetBranchRef()]
1418 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001419 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001420 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001422 # We want to squash all this branch's commits into one commit with the proper
1423 # description. We do this by doing a "reset --soft" to the base branch (which
1424 # keeps the working copy the same), then dcommitting that. If origin/master
1425 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1426 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001428 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1429 # Delete the branches if they exist.
1430 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1431 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1432 result = RunGitWithCode(showref_cmd)
1433 if result[0] == 0:
1434 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435
1436 # We might be in a directory that's present in this branch but not in the
1437 # trunk. Move up to the top of the tree so that git commands that expect a
1438 # valid CWD won't fail after we check out the merge branch.
1439 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1440 if rel_base_path:
1441 os.chdir(rel_base_path)
1442
1443 # Stuff our change into the merge branch.
1444 # We wrap in a try...finally block so if anything goes wrong,
1445 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001446 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001448 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1449 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 if options.contributor:
1451 RunGit(['commit', '--author', options.contributor, '-m', description])
1452 else:
1453 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001454 if base_has_submodules:
1455 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1456 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1457 RunGit(['checkout', CHERRY_PICK_BRANCH])
1458 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 if cmd == 'push':
1460 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001461 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 retcode, output = RunGitWithCode(
1463 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1464 logging.debug(output)
1465 else:
1466 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001467 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001468 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001469 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 finally:
1471 # And then swap back to the original branch and clean up.
1472 RunGit(['checkout', '-q', cl.GetBranch()])
1473 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001474 if base_has_submodules:
1475 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476
1477 if cl.GetIssue():
1478 if cmd == 'dcommit' and 'Committed r' in output:
1479 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1480 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001481 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1482 for l in output.splitlines(False))
1483 match = filter(None, match)
1484 if len(match) != 1:
1485 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1486 output)
1487 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 else:
1489 return 1
1490 viewvc_url = settings.GetViewVCUrl()
1491 if viewvc_url and revision:
1492 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001493 elif revision:
1494 cl.description += ('\n\nCommitted: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 print ('Closing issue '
1496 '(you may be prompted for your codereview password)...')
1497 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001498 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001499 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001500 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001501 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1502 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001504
1505 if retcode == 0:
1506 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1507 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001508 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001509
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510 return 0
1511
1512
1513@usage('[upstream branch to apply against]')
1514def CMDdcommit(parser, args):
1515 """commit the current changelist via git-svn"""
1516 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001517 message = """This doesn't appear to be an SVN repository.
1518If your project has a git mirror with an upstream SVN master, you probably need
1519to run 'git svn init', see your project's git mirror documentation.
1520If your project has a true writeable upstream repository, you probably want
1521to run 'git cl push' instead.
1522Choose wisely, if you get this wrong, your commit might appear to succeed but
1523will instead be silently ignored."""
1524 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001525 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526 return SendUpstream(parser, args, 'dcommit')
1527
1528
1529@usage('[upstream branch to apply against]')
1530def CMDpush(parser, args):
1531 """commit the current changelist via git"""
1532 if settings.GetIsGitSvn():
1533 print('This appears to be an SVN repository.')
1534 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001535 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 return SendUpstream(parser, args, 'push')
1537
1538
1539@usage('<patch url or issue id>')
1540def CMDpatch(parser, args):
1541 """patch in a code review"""
1542 parser.add_option('-b', dest='newbranch',
1543 help='create a new branch off trunk for the patch')
1544 parser.add_option('-f', action='store_true', dest='force',
1545 help='with -b, clobber any existing branch')
1546 parser.add_option('--reject', action='store_true', dest='reject',
1547 help='allow failed patches and spew .rej files')
1548 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1549 help="don't commit after patch applies")
1550 (options, args) = parser.parse_args(args)
1551 if len(args) != 1:
1552 parser.print_help()
1553 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001554 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001555
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001556 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001557 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001558
maruel@chromium.org52424302012-08-29 15:14:30 +00001559 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001561 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001562 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001563 patchset = cl.GetMostRecentPatchset(issue)
1564 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001566 # Assume it's a URL to the patch. Default to https.
1567 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001568 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001569 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001570 DieWithError('Must pass an issue ID or full URL for '
1571 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001572 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001573 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001574 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575
1576 if options.newbranch:
1577 if options.force:
1578 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001579 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 RunGit(['checkout', '-b', options.newbranch,
1581 Changelist().GetUpstreamBranch()])
1582
1583 # Switch up to the top-level directory, if necessary, in preparation for
1584 # applying the patch.
1585 top = RunGit(['rev-parse', '--show-cdup']).strip()
1586 if top:
1587 os.chdir(top)
1588
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589 # Git patches have a/ at the beginning of source paths. We strip that out
1590 # with a sed script rather than the -p flag to patch so we can feed either
1591 # Git or svn-style patches into the same apply command.
1592 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001593 try:
1594 patch_data = subprocess2.check_output(
1595 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1596 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 DieWithError('Git patch mungling failed.')
1598 logging.info(patch_data)
1599 # We use "git apply" to apply the patch instead of "patch" so that we can
1600 # pick up file adds.
1601 # The --index flag means: also insert into the index (so we catch adds).
1602 cmd = ['git', 'apply', '--index', '-p0']
1603 if options.reject:
1604 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001605 try:
1606 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1607 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001608 DieWithError('Failed to apply the patch')
1609
1610 # If we had an issue, commit the current state and register the issue.
1611 if not options.nocommit:
1612 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1613 cl = Changelist()
1614 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001615 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616 print "Committed patch."
1617 else:
1618 print "Patch applied to index."
1619 return 0
1620
1621
1622def CMDrebase(parser, args):
1623 """rebase current branch on top of svn repo"""
1624 # Provide a wrapper for git svn rebase to help avoid accidental
1625 # git svn dcommit.
1626 # It's the only command that doesn't use parser at all since we just defer
1627 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001628 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001629
1630
1631def GetTreeStatus():
1632 """Fetches the tree status and returns either 'open', 'closed',
1633 'unknown' or 'unset'."""
1634 url = settings.GetTreeStatusUrl(error_ok=True)
1635 if url:
1636 status = urllib2.urlopen(url).read().lower()
1637 if status.find('closed') != -1 or status == '0':
1638 return 'closed'
1639 elif status.find('open') != -1 or status == '1':
1640 return 'open'
1641 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 return 'unset'
1643
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645def GetTreeStatusReason():
1646 """Fetches the tree status from a json url and returns the message
1647 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001648 url = settings.GetTreeStatusUrl()
1649 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650 connection = urllib2.urlopen(json_url)
1651 status = json.loads(connection.read())
1652 connection.close()
1653 return status['message']
1654
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656def CMDtree(parser, args):
1657 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001658 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001659 status = GetTreeStatus()
1660 if 'unset' == status:
1661 print 'You must configure your tree status URL by running "git cl config".'
1662 return 2
1663
1664 print "The tree is %s" % status
1665 print
1666 print GetTreeStatusReason()
1667 if status != 'open':
1668 return 1
1669 return 0
1670
1671
maruel@chromium.org15192402012-09-06 12:38:29 +00001672def CMDtry(parser, args):
1673 """Triggers a try job through Rietveld."""
1674 group = optparse.OptionGroup(parser, "Try job options")
1675 group.add_option(
1676 "-b", "--bot", action="append",
1677 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1678 "times to specify multiple builders. ex: "
1679 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1680 "the try server waterfall for the builders name and the tests "
1681 "available. Can also be used to specify gtest_filter, e.g. "
1682 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1683 group.add_option(
1684 "-r", "--revision",
1685 help="Revision to use for the try job; default: the "
1686 "revision will be determined by the try server; see "
1687 "its waterfall for more info")
1688 group.add_option(
1689 "-c", "--clobber", action="store_true", default=False,
1690 help="Force a clobber before building; e.g. don't do an "
1691 "incremental build")
1692 group.add_option(
1693 "--project",
1694 help="Override which project to use. Projects are defined "
1695 "server-side to define what default bot set to use")
1696 group.add_option(
1697 "-t", "--testfilter", action="append", default=[],
1698 help=("Apply a testfilter to all the selected builders. Unless the "
1699 "builders configurations are similar, use multiple "
1700 "--bot <builder>:<test> arguments."))
1701 group.add_option(
1702 "-n", "--name", help="Try job name; default to current branch name")
1703 parser.add_option_group(group)
1704 options, args = parser.parse_args(args)
1705
1706 if args:
1707 parser.error('Unknown arguments: %s' % args)
1708
1709 cl = Changelist()
1710 if not cl.GetIssue():
1711 parser.error('Need to upload first')
1712
1713 if not options.name:
1714 options.name = cl.GetBranch()
1715
1716 # Process --bot and --testfilter.
1717 if not options.bot:
1718 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001719 change = cl.GetChange(
1720 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1721 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001722 options.bot = presubmit_support.DoGetTrySlaves(
1723 change,
1724 change.LocalPaths(),
1725 settings.GetRoot(),
1726 None,
1727 None,
1728 options.verbose,
1729 sys.stdout)
1730 if not options.bot:
1731 parser.error('No default try builder to try, use --bot')
1732
1733 builders_and_tests = {}
1734 for bot in options.bot:
1735 if ':' in bot:
1736 builder, tests = bot.split(':', 1)
1737 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1738 elif ',' in bot:
1739 parser.error('Specify one bot per --bot flag')
1740 else:
1741 builders_and_tests.setdefault(bot, []).append('defaulttests')
1742
1743 if options.testfilter:
1744 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1745 builders_and_tests = dict(
1746 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1747 if t != ['compile'])
1748
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001749 if any('triggered' in b for b in builders_and_tests):
1750 print >> sys.stderr, (
1751 'ERROR You are trying to send a job to a triggered bot. This type of'
1752 ' bot requires an\ninitial job from a parent (usually a builder). '
1753 'Instead send your job to the parent.\n'
1754 'Bot list: %s' % builders_and_tests)
1755 return 1
1756
maruel@chromium.org15192402012-09-06 12:38:29 +00001757 patchset = cl.GetPatchset()
1758 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001759 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001760
1761 cl.RpcServer().trigger_try_jobs(
1762 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1763 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001764 print('Tried jobs on:')
1765 length = max(len(builder) for builder in builders_and_tests)
1766 for builder in sorted(builders_and_tests):
1767 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001768 return 0
1769
1770
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001771@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001773 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001774 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001775 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001776 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001777 return 0
1778
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001780 if args:
1781 # One arg means set upstream branch.
1782 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1783 cl = Changelist()
1784 print "Upstream branch set to " + cl.GetUpstreamBranch()
1785 else:
1786 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001787 return 0
1788
1789
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001790def CMDset_commit(parser, args):
1791 """set the commit bit"""
1792 _, args = parser.parse_args(args)
1793 if args:
1794 parser.error('Unrecognized args: %s' % ' '.join(args))
1795 cl = Changelist()
1796 cl.SetFlag('commit', '1')
1797 return 0
1798
1799
groby@chromium.org411034a2013-02-26 15:12:01 +00001800def CMDset_close(parser, args):
1801 """close the issue"""
1802 _, args = parser.parse_args(args)
1803 if args:
1804 parser.error('Unrecognized args: %s' % ' '.join(args))
1805 cl = Changelist()
1806 # Ensure there actually is an issue to close.
1807 cl.GetDescription()
1808 cl.CloseIssue()
1809 return 0
1810
1811
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812def Command(name):
1813 return getattr(sys.modules[__name__], 'CMD' + name, None)
1814
1815
1816def CMDhelp(parser, args):
1817 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001818 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819 if len(args) == 1:
1820 return main(args + ['--help'])
1821 parser.print_help()
1822 return 0
1823
1824
1825def GenUsage(parser, command):
1826 """Modify an OptParse object with the function's documentation."""
1827 obj = Command(command)
1828 more = getattr(obj, 'usage_more', '')
1829 if command == 'help':
1830 command = '<command>'
1831 else:
1832 # OptParser.description prefer nicely non-formatted strings.
1833 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1834 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1835
1836
1837def main(argv):
1838 """Doesn't parse the arguments here, just find the right subcommand to
1839 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001840 if sys.hexversion < 0x02060000:
1841 print >> sys.stderr, (
1842 '\nYour python version %s is unsupported, please upgrade.\n' %
1843 sys.version.split(' ', 1)[0])
1844 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001845 # Reload settings.
1846 global settings
1847 settings = Settings()
1848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 # Do it late so all commands are listed.
1850 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1851 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1852 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1853
1854 # Create the option parse and add --verbose support.
1855 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001856 parser.add_option(
1857 '-v', '--verbose', action='count', default=0,
1858 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859 old_parser_args = parser.parse_args
1860 def Parse(args):
1861 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001862 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001864 elif options.verbose:
1865 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001866 else:
1867 logging.basicConfig(level=logging.WARNING)
1868 return options, args
1869 parser.parse_args = Parse
1870
1871 if argv:
1872 command = Command(argv[0])
1873 if command:
1874 # "fix" the usage and the description now that we know the subcommand.
1875 GenUsage(parser, argv[0])
1876 try:
1877 return command(parser, argv[1:])
1878 except urllib2.HTTPError, e:
1879 if e.code != 500:
1880 raise
1881 DieWithError(
1882 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1883 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1884
1885 # Not a known command. Default to help.
1886 GenUsage(parser, 'help')
1887 return CMDhelp(parser, argv)
1888
1889
1890if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001891 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892 sys.exit(main(sys.argv[1:]))