blob: 35cfea3b7d3d9bb1d17df413008890c114b897b8 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
27from third_party import upload
28import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000029import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000032import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000034import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import watchlists
36
37
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000038DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000039POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000041GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000042CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000043
maruel@chromium.org90541732011-04-01 17:54:18 +000044
maruel@chromium.orgddd59412011-11-30 14:20:38 +000045# Initialized in main()
46settings = None
47
48
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000050 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051 sys.exit(1)
52
53
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000056 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057 except subprocess2.CalledProcessError, e:
58 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060 'Command "%s" failed.\n%s' % (
61 ' '.join(args), error_message or e.stdout or ''))
62 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063
64
65def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 """Returns stdout."""
67 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
69
70def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000072 try:
73 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
74 return code, out[0]
75 except ValueError:
76 # When the subprocess fails, it returns None. That triggers a ValueError
77 # when trying to unpack the return value into (out, code).
78 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
80
81def usage(more):
82 def hook(fn):
83 fn.usage_more = more
84 return fn
85 return hook
86
87
maruel@chromium.org90541732011-04-01 17:54:18 +000088def ask_for_data(prompt):
89 try:
90 return raw_input(prompt)
91 except KeyboardInterrupt:
92 # Hide the exception.
93 sys.exit(1)
94
95
iannucci@chromium.org79540052012-10-19 23:15:26 +000096def git_set_branch_value(key, value):
97 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +000098 if not branch:
99 return
100
101 cmd = ['config']
102 if isinstance(value, int):
103 cmd.append('--int')
104 git_key = 'branch.%s.%s' % (branch, key)
105 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000106
107
108def git_get_branch_default(key, default):
109 branch = Changelist().GetBranch()
110 if branch:
111 git_key = 'branch.%s.%s' % (branch, key)
112 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
113 try:
114 return int(stdout.strip())
115 except ValueError:
116 pass
117 return default
118
119
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000120def add_git_similarity(parser):
121 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000122 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000123 help='Sets the percentage that a pair of files need to match in order to'
124 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000125 parser.add_option(
126 '--find-copies', action='store_true',
127 help='Allows git to look for copies.')
128 parser.add_option(
129 '--no-find-copies', action='store_false', dest='find_copies',
130 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000131
132 old_parser_args = parser.parse_args
133 def Parse(args):
134 options, args = old_parser_args(args)
135
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000136 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000137 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000138 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 print('Note: Saving similarity of %d%% in git config.'
140 % options.similarity)
141 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000142
iannucci@chromium.org79540052012-10-19 23:15:26 +0000143 options.similarity = max(0, min(options.similarity, 100))
144
145 if options.find_copies is None:
146 options.find_copies = bool(
147 git_get_branch_default('git-find-copies', True))
148 else:
149 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000150
151 print('Using %d%% similarity for rename/copy detection. '
152 'Override with --similarity.' % options.similarity)
153
154 return options, args
155 parser.parse_args = Parse
156
157
ukai@chromium.org259e4682012-10-25 07:36:33 +0000158def is_dirty_git_tree(cmd):
159 # Make sure index is up-to-date before running diff-index.
160 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
161 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
162 if dirty:
163 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
164 print 'Uncommitted files: (git diff-index --name-status HEAD)'
165 print dirty[:4096]
166 if len(dirty) > 4096:
167 print '... (run "git diff-index --name-status HEAD" to see full output).'
168 return True
169 return False
170
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000171
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000172def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
173 """Return the corresponding git ref if |base_url| together with |glob_spec|
174 matches the full |url|.
175
176 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
177 """
178 fetch_suburl, as_ref = glob_spec.split(':')
179 if allow_wildcards:
180 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
181 if glob_match:
182 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
183 # "branches/{472,597,648}/src:refs/remotes/svn/*".
184 branch_re = re.escape(base_url)
185 if glob_match.group(1):
186 branch_re += '/' + re.escape(glob_match.group(1))
187 wildcard = glob_match.group(2)
188 if wildcard == '*':
189 branch_re += '([^/]*)'
190 else:
191 # Escape and replace surrounding braces with parentheses and commas
192 # with pipe symbols.
193 wildcard = re.escape(wildcard)
194 wildcard = re.sub('^\\\\{', '(', wildcard)
195 wildcard = re.sub('\\\\,', '|', wildcard)
196 wildcard = re.sub('\\\\}$', ')', wildcard)
197 branch_re += wildcard
198 if glob_match.group(3):
199 branch_re += re.escape(glob_match.group(3))
200 match = re.match(branch_re, url)
201 if match:
202 return re.sub('\*$', match.group(1), as_ref)
203
204 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
205 if fetch_suburl:
206 full_url = base_url + '/' + fetch_suburl
207 else:
208 full_url = base_url
209 if full_url == url:
210 return as_ref
211 return None
212
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000213
iannucci@chromium.org79540052012-10-19 23:15:26 +0000214def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000215 """Prints statistics about the change to the user."""
216 # --no-ext-diff is broken in some versions of Git, so try to work around
217 # this by overriding the environment (but there is still a problem if the
218 # git config key "diff.external" is used).
219 env = os.environ.copy()
220 if 'GIT_EXTERNAL_DIFF' in env:
221 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000222
223 if find_copies:
224 similarity_options = ['--find-copies-harder', '-l100000',
225 '-C%s' % similarity]
226 else:
227 similarity_options = ['-M%s' % similarity]
228
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000229 return subprocess2.call(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000230 ['git', 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
231 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000232
233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000234class Settings(object):
235 def __init__(self):
236 self.default_server = None
237 self.cc = None
238 self.root = None
239 self.is_git_svn = None
240 self.svn_branch = None
241 self.tree_status_url = None
242 self.viewvc_url = None
243 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000244 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000245
246 def LazyUpdateIfNeeded(self):
247 """Updates the settings from a codereview.settings file, if available."""
248 if not self.updated:
249 cr_settings_file = FindCodereviewSettingsFile()
250 if cr_settings_file:
251 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000252 self.updated = True
253 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000254 self.updated = True
255
256 def GetDefaultServerUrl(self, error_ok=False):
257 if not self.default_server:
258 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000259 self.default_server = gclient_utils.UpgradeToHttps(
260 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000261 if error_ok:
262 return self.default_server
263 if not self.default_server:
264 error_message = ('Could not find settings file. You must configure '
265 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000266 self.default_server = gclient_utils.UpgradeToHttps(
267 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268 return self.default_server
269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000270 def GetRoot(self):
271 if not self.root:
272 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
273 return self.root
274
275 def GetIsGitSvn(self):
276 """Return true if this repo looks like it's using git-svn."""
277 if self.is_git_svn is None:
278 # If you have any "svn-remote.*" config keys, we think you're using svn.
279 self.is_git_svn = RunGitWithCode(
280 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
281 return self.is_git_svn
282
283 def GetSVNBranch(self):
284 if self.svn_branch is None:
285 if not self.GetIsGitSvn():
286 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
287
288 # Try to figure out which remote branch we're based on.
289 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000290 # 1) iterate through our branch history and find the svn URL.
291 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292
293 # regexp matching the git-svn line that contains the URL.
294 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
295
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000296 # We don't want to go through all of history, so read a line from the
297 # pipe at a time.
298 # The -100 is an arbitrary limit so we don't search forever.
299 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000300 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000301 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000302 for line in proc.stdout:
303 match = git_svn_re.match(line)
304 if match:
305 url = match.group(1)
306 proc.stdout.close() # Cut pipe.
307 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000308
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000309 if url:
310 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
311 remotes = RunGit(['config', '--get-regexp',
312 r'^svn-remote\..*\.url']).splitlines()
313 for remote in remotes:
314 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000315 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000316 remote = match.group(1)
317 base_url = match.group(2)
318 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000319 ['config', 'svn-remote.%s.fetch' % remote],
320 error_ok=True).strip()
321 if fetch_spec:
322 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
323 if self.svn_branch:
324 break
325 branch_spec = RunGit(
326 ['config', 'svn-remote.%s.branches' % remote],
327 error_ok=True).strip()
328 if branch_spec:
329 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
330 if self.svn_branch:
331 break
332 tag_spec = RunGit(
333 ['config', 'svn-remote.%s.tags' % remote],
334 error_ok=True).strip()
335 if tag_spec:
336 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
337 if self.svn_branch:
338 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000339
340 if not self.svn_branch:
341 DieWithError('Can\'t guess svn branch -- try specifying it on the '
342 'command line')
343
344 return self.svn_branch
345
346 def GetTreeStatusUrl(self, error_ok=False):
347 if not self.tree_status_url:
348 error_message = ('You must configure your tree status URL by running '
349 '"git cl config".')
350 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
351 error_ok=error_ok,
352 error_message=error_message)
353 return self.tree_status_url
354
355 def GetViewVCUrl(self):
356 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000357 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000358 return self.viewvc_url
359
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000360 def GetDefaultCCList(self):
361 return self._GetConfig('rietveld.cc', error_ok=True)
362
ukai@chromium.orge8077812012-02-03 03:41:46 +0000363 def GetIsGerrit(self):
364 """Return true if this repo is assosiated with gerrit code review system."""
365 if self.is_gerrit is None:
366 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
367 return self.is_gerrit
368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000369 def _GetConfig(self, param, **kwargs):
370 self.LazyUpdateIfNeeded()
371 return RunGit(['config', param], **kwargs).strip()
372
373
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000374def ShortBranchName(branch):
375 """Convert a name like 'refs/heads/foo' to just 'foo'."""
376 return branch.replace('refs/heads/', '')
377
378
379class Changelist(object):
380 def __init__(self, branchref=None):
381 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000382 global settings
383 if not settings:
384 # Happens when git_cl.py is used as a utility library.
385 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000386 settings.GetDefaultServerUrl()
387 self.branchref = branchref
388 if self.branchref:
389 self.branch = ShortBranchName(self.branchref)
390 else:
391 self.branch = None
392 self.rietveld_server = None
393 self.upstream_branch = None
394 self.has_issue = False
395 self.issue = None
396 self.has_description = False
397 self.description = None
398 self.has_patchset = False
399 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000400 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000401 self.cc = None
402 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000403 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000404
405 def GetCCList(self):
406 """Return the users cc'd on this CL.
407
408 Return is a string suitable for passing to gcl with the --cc flag.
409 """
410 if self.cc is None:
411 base_cc = settings .GetDefaultCCList()
412 more_cc = ','.join(self.watchers)
413 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
414 return self.cc
415
416 def SetWatchers(self, watchers):
417 """Set the list of email addresses that should be cc'd based on the changed
418 files in this CL.
419 """
420 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000421
422 def GetBranch(self):
423 """Returns the short branch name, e.g. 'master'."""
424 if not self.branch:
425 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
426 self.branch = ShortBranchName(self.branchref)
427 return self.branch
428
429 def GetBranchRef(self):
430 """Returns the full branch name, e.g. 'refs/heads/master'."""
431 self.GetBranch() # Poke the lazy loader.
432 return self.branchref
433
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000434 @staticmethod
435 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 """Returns a tuple containg remote and remote ref,
437 e.g. 'origin', 'refs/heads/master'
438 """
439 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
441 error_ok=True).strip()
442 if upstream_branch:
443 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
444 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000445 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
446 error_ok=True).strip()
447 if upstream_branch:
448 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000450 # Fall back on trying a git-svn upstream branch.
451 if settings.GetIsGitSvn():
452 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000454 # Else, try to guess the origin remote.
455 remote_branches = RunGit(['branch', '-r']).split()
456 if 'origin/master' in remote_branches:
457 # Fall back on origin/master if it exits.
458 remote = 'origin'
459 upstream_branch = 'refs/heads/master'
460 elif 'origin/trunk' in remote_branches:
461 # Fall back on origin/trunk if it exists. Generally a shared
462 # git-svn clone
463 remote = 'origin'
464 upstream_branch = 'refs/heads/trunk'
465 else:
466 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000467Either pass complete "git diff"-style arguments, like
468 git cl upload origin/master
469or verify this branch is set up to track another (via the --track argument to
470"git checkout -b ...").""")
471
472 return remote, upstream_branch
473
474 def GetUpstreamBranch(self):
475 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000476 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477 if remote is not '.':
478 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
479 self.upstream_branch = upstream_branch
480 return self.upstream_branch
481
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000482 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000483 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000484 remote, branch = None, self.GetBranch()
485 seen_branches = set()
486 while branch not in seen_branches:
487 seen_branches.add(branch)
488 remote, branch = self.FetchUpstreamTuple(branch)
489 branch = ShortBranchName(branch)
490 if remote != '.' or branch.startswith('refs/remotes'):
491 break
492 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000493 remotes = RunGit(['remote'], error_ok=True).split()
494 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000495 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000496 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000497 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000498 logging.warning('Could not determine which remote this change is '
499 'associated with, so defaulting to "%s". This may '
500 'not be what you want. You may prevent this message '
501 'by running "git svn info" as documented here: %s',
502 self._remote,
503 GIT_INSTRUCTIONS_URL)
504 else:
505 logging.warn('Could not determine which remote this change is '
506 'associated with. You may prevent this message by '
507 'running "git svn info" as documented here: %s',
508 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000509 branch = 'HEAD'
510 if branch.startswith('refs/remotes'):
511 self._remote = (remote, branch)
512 else:
513 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000514 return self._remote
515
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 def GitSanityChecks(self, upstream_git_obj):
517 """Checks git repo status and ensures diff is from local commits."""
518
519 # Verify the commit we're diffing against is in our current branch.
520 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
521 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
522 if upstream_sha != common_ancestor:
523 print >> sys.stderr, (
524 'ERROR: %s is not in the current branch. You may need to rebase '
525 'your tracking branch' % upstream_sha)
526 return False
527
528 # List the commits inside the diff, and verify they are all local.
529 commits_in_diff = RunGit(
530 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
531 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
532 remote_branch = remote_branch.strip()
533 if code != 0:
534 _, remote_branch = self.GetRemoteBranch()
535
536 commits_in_remote = RunGit(
537 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
538
539 common_commits = set(commits_in_diff) & set(commits_in_remote)
540 if common_commits:
541 print >> sys.stderr, (
542 'ERROR: Your diff contains %d commits already in %s.\n'
543 'Run "git log --oneline %s..HEAD" to get a list of commits in '
544 'the diff. If you are using a custom git flow, you can override'
545 ' the reference used for this check with "git config '
546 'gitcl.remotebranch <git-ref>".' % (
547 len(common_commits), remote_branch, upstream_git_obj))
548 return False
549 return True
550
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000551 def GetGitBaseUrlFromConfig(self):
552 """Return the configured base URL from branch.<branchname>.baseurl.
553
554 Returns None if it is not set.
555 """
556 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
557 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000558
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000559 def GetRemoteUrl(self):
560 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
561
562 Returns None if there is no remote.
563 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000564 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000565 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
566
567 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000568 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
571 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000572 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573 else:
574 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 self.has_issue = True
576 return self.issue
577
578 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000579 if not self.rietveld_server:
580 # If we're on a branch then get the server potentially associated
581 # with that branch.
582 if self.GetIssue():
583 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
584 ['config', self._RietveldServer()], error_ok=True).strip())
585 if not self.rietveld_server:
586 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 return self.rietveld_server
588
589 def GetIssueURL(self):
590 """Get the URL for a particular issue."""
591 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
592
593 def GetDescription(self, pretty=False):
594 if not self.has_description:
595 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000596 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000597 try:
598 self.description = self.RpcServer().get_description(issue).strip()
599 except urllib2.HTTPError, e:
600 if e.code == 404:
601 DieWithError(
602 ('\nWhile fetching the description for issue %d, received a '
603 '404 (not found)\n'
604 'error. It is likely that you deleted this '
605 'issue on the server. If this is the\n'
606 'case, please run\n\n'
607 ' git cl issue 0\n\n'
608 'to clear the association with the deleted issue. Then run '
609 'this command again.') % issue)
610 else:
611 DieWithError(
612 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 self.has_description = True
614 if pretty:
615 wrapper = textwrap.TextWrapper()
616 wrapper.initial_indent = wrapper.subsequent_indent = ' '
617 return wrapper.fill(self.description)
618 return self.description
619
620 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000621 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 if not self.has_patchset:
623 patchset = RunGit(['config', self._PatchsetSetting()],
624 error_ok=True).strip()
625 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000626 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 else:
628 self.patchset = None
629 self.has_patchset = True
630 return self.patchset
631
632 def SetPatchset(self, patchset):
633 """Set this branch's patchset. If patchset=0, clears the patchset."""
634 if patchset:
635 RunGit(['config', self._PatchsetSetting(), str(patchset)])
636 else:
637 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000638 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639 self.has_patchset = False
640
binji@chromium.org0281f522012-09-14 13:37:59 +0000641 def GetMostRecentPatchset(self, issue):
642 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000643 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000644
645 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000646 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000647 '/download/issue%s_%s.diff' % (issue, patchset))
648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 def SetIssue(self, issue):
650 """Set this branch's issue. If issue=0, clears the issue."""
651 if issue:
652 RunGit(['config', self._IssueSetting(), str(issue)])
653 if self.rietveld_server:
654 RunGit(['config', self._RietveldServer(), self.rietveld_server])
655 else:
656 RunGit(['config', '--unset', self._IssueSetting()])
657 self.SetPatchset(0)
658 self.has_issue = False
659
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000660 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000661 if not self.GitSanityChecks(upstream_branch):
662 DieWithError('\nGit sanity check failure')
663
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000664 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
665 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000666
667 # We use the sha1 of HEAD as a name of this change.
668 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000669 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000670 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000671 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000672 except subprocess2.CalledProcessError:
673 DieWithError(
674 ('\nFailed to diff against upstream branch %s!\n\n'
675 'This branch probably doesn\'t exist anymore. To reset the\n'
676 'tracking branch, please run\n'
677 ' git branch --set-upstream %s trunk\n'
678 'replacing trunk with origin/master or the relevant branch') %
679 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000680
maruel@chromium.org52424302012-08-29 15:14:30 +0000681 issue = self.GetIssue()
682 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000683 if issue:
684 description = self.GetDescription()
685 else:
686 # If the change was never uploaded, use the log messages of all commits
687 # up to the branch point, as git cl upload will prefill the description
688 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000689 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
690 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000691
692 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000693 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000694 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000695 name,
696 description,
697 absroot,
698 files,
699 issue,
700 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000701 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000702
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000703 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000704 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000705
706 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000707 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000708 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000709 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000710 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000711 except presubmit_support.PresubmitFailure, e:
712 DieWithError(
713 ('%s\nMaybe your depot_tools is out of date?\n'
714 'If all fails, contact maruel@') % e)
715
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000716 def UpdateDescription(self, description):
717 self.description = description
718 return self.RpcServer().update_description(
719 self.GetIssue(), self.description)
720
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000722 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000723 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000725 def SetFlag(self, flag, value):
726 """Patchset must match."""
727 if not self.GetPatchset():
728 DieWithError('The patchset needs to match. Send another patchset.')
729 try:
730 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000731 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000732 except urllib2.HTTPError, e:
733 if e.code == 404:
734 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
735 if e.code == 403:
736 DieWithError(
737 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
738 'match?') % (self.GetIssue(), self.GetPatchset()))
739 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000741 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742 """Returns an upload.RpcServer() to access this review's rietveld instance.
743 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000744 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000745 self._rpc_server = rietveld.CachingRietveld(
746 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000747 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748
749 def _IssueSetting(self):
750 """Return the git setting that stores this change's issue."""
751 return 'branch.%s.rietveldissue' % self.GetBranch()
752
753 def _PatchsetSetting(self):
754 """Return the git setting that stores this change's most recent patchset."""
755 return 'branch.%s.rietveldpatchset' % self.GetBranch()
756
757 def _RietveldServer(self):
758 """Returns the git setting that stores this change's rietveld server."""
759 return 'branch.%s.rietveldserver' % self.GetBranch()
760
761
762def GetCodereviewSettingsInteractively():
763 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000764 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 server = settings.GetDefaultServerUrl(error_ok=True)
766 prompt = 'Rietveld server (host[:port])'
767 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000768 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769 if not server and not newserver:
770 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000771 if newserver:
772 newserver = gclient_utils.UpgradeToHttps(newserver)
773 if newserver != server:
774 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000776 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 prompt = caption
778 if initial:
779 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000780 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781 if new_val == 'x':
782 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000783 elif new_val:
784 if is_url:
785 new_val = gclient_utils.UpgradeToHttps(new_val)
786 if new_val != initial:
787 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000789 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000791 'tree-status-url', False)
792 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793
794 # TODO: configure a default branch to diff against, rather than this
795 # svn-based hackery.
796
797
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000798class ChangeDescription(object):
799 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000800 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000801 self.log_desc = log_desc
802 self.reviewers = reviewers
803 self.description = self.log_desc
804
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000805 def Prompt(self):
806 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000807# This will displayed on the codereview site.
808# The first line will also be used as the subject of the review.
809"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000810 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000811 if ('\nR=' not in self.description and
812 '\nTBR=' not in self.description and
813 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000814 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000815 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000816 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000817 content = content.rstrip('\n') + '\n'
818 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000819 if not content:
820 DieWithError('Running editor failed')
821 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000822 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000823 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000824 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000825
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000826 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000827 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000828 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000829 # Retrieves all reviewer lines
830 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +0000831 reviewers = ', '.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000832 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000833 if reviewers:
834 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000835
836 def IsEmpty(self):
837 return not self.description
838
839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840def FindCodereviewSettingsFile(filename='codereview.settings'):
841 """Finds the given file starting in the cwd and going up.
842
843 Only looks up to the top of the repository unless an
844 'inherit-review-settings-ok' file exists in the root of the repository.
845 """
846 inherit_ok_file = 'inherit-review-settings-ok'
847 cwd = os.getcwd()
848 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
849 if os.path.isfile(os.path.join(root, inherit_ok_file)):
850 root = '/'
851 while True:
852 if filename in os.listdir(cwd):
853 if os.path.isfile(os.path.join(cwd, filename)):
854 return open(os.path.join(cwd, filename))
855 if cwd == root:
856 break
857 cwd = os.path.dirname(cwd)
858
859
860def LoadCodereviewSettingsFromFile(fileobj):
861 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000862 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864 def SetProperty(name, setting, unset_error_ok=False):
865 fullname = 'rietveld.' + name
866 if setting in keyvals:
867 RunGit(['config', fullname, keyvals[setting]])
868 else:
869 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
870
871 SetProperty('server', 'CODE_REVIEW_SERVER')
872 # Only server setting is required. Other settings can be absent.
873 # In that case, we ignore errors raised during option deletion attempt.
874 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
875 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
876 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
877
ukai@chromium.orge8077812012-02-03 03:41:46 +0000878 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
879 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
880 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
883 #should be of the form
884 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
885 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
886 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
887 keyvals['ORIGIN_URL_CONFIG']])
888
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000890def urlretrieve(source, destination):
891 """urllib is broken for SSL connections via a proxy therefore we
892 can't use urllib.urlretrieve()."""
893 with open(destination, 'w') as f:
894 f.write(urllib2.urlopen(source).read())
895
896
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000897def DownloadHooks(force):
898 """downloads hooks
899
900 Args:
901 force: True to update hooks. False to install hooks if not present.
902 """
903 if not settings.GetIsGerrit():
904 return
905 server_url = settings.GetDefaultServerUrl()
906 src = '%s/tools/hooks/commit-msg' % server_url
907 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
908 if not os.access(dst, os.X_OK):
909 if os.path.exists(dst):
910 if not force:
911 return
912 os.remove(dst)
913 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000914 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000915 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
916 except Exception:
917 if os.path.exists(dst):
918 os.remove(dst)
919 DieWithError('\nFailed to download hooks from %s' % src)
920
921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922@usage('[repo root containing codereview.settings]')
923def CMDconfig(parser, args):
924 """edit configuration for this tree"""
925
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000926 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 if len(args) == 0:
928 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000929 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 return 0
931
932 url = args[0]
933 if not url.endswith('codereview.settings'):
934 url = os.path.join(url, 'codereview.settings')
935
936 # Load code review settings and download hooks (if available).
937 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000938 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 return 0
940
941
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000942def CMDbaseurl(parser, args):
943 """get or set base-url for this branch"""
944 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
945 branch = ShortBranchName(branchref)
946 _, args = parser.parse_args(args)
947 if not args:
948 print("Current base-url:")
949 return RunGit(['config', 'branch.%s.base-url' % branch],
950 error_ok=False).strip()
951 else:
952 print("Setting base-url to %s" % args[0])
953 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
954 error_ok=False).strip()
955
956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957def CMDstatus(parser, args):
958 """show status of changelists"""
959 parser.add_option('--field',
960 help='print only specific field (desc|id|patch|url)')
961 (options, args) = parser.parse_args(args)
962
963 # TODO: maybe make show_branches a flag if necessary.
964 show_branches = not options.field
965
966 if show_branches:
967 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
968 if branches:
969 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000970 changes = (Changelist(branchref=b) for b in branches.splitlines())
971 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
972 alignment = max(5, max(len(b) for b in branches))
973 for branch in sorted(branches):
974 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975
976 cl = Changelist()
977 if options.field:
978 if options.field.startswith('desc'):
979 print cl.GetDescription()
980 elif options.field == 'id':
981 issueid = cl.GetIssue()
982 if issueid:
983 print issueid
984 elif options.field == 'patch':
985 patchset = cl.GetPatchset()
986 if patchset:
987 print patchset
988 elif options.field == 'url':
989 url = cl.GetIssueURL()
990 if url:
991 print url
992 else:
993 print
994 print 'Current branch:',
995 if not cl.GetIssue():
996 print 'no issue assigned.'
997 return 0
998 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000999 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 print 'Issue description:'
1001 print cl.GetDescription(pretty=True)
1002 return 0
1003
1004
1005@usage('[issue_number]')
1006def CMDissue(parser, args):
1007 """Set or display the current code review issue number.
1008
1009 Pass issue number 0 to clear the current issue.
1010"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001011 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012
1013 cl = Changelist()
1014 if len(args) > 0:
1015 try:
1016 issue = int(args[0])
1017 except ValueError:
1018 DieWithError('Pass a number to set the issue or none to list it.\n'
1019 'Maybe you want to run git cl status?')
1020 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001021 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022 return 0
1023
1024
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001025def CMDcomments(parser, args):
1026 """show review comments of the current changelist"""
1027 (_, args) = parser.parse_args(args)
1028 if args:
1029 parser.error('Unsupported argument: %s' % args)
1030
1031 cl = Changelist()
1032 if cl.GetIssue():
1033 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1034 for message in sorted(data['messages'], key=lambda x: x['date']):
1035 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1036 if message['text'].strip():
1037 print '\n'.join(' ' + l for l in message['text'].splitlines())
1038 return 0
1039
1040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041def CreateDescriptionFromLog(args):
1042 """Pulls out the commit log to use as a base for the CL description."""
1043 log_args = []
1044 if len(args) == 1 and not args[0].endswith('.'):
1045 log_args = [args[0] + '..']
1046 elif len(args) == 1 and args[0].endswith('...'):
1047 log_args = [args[0][:-1]]
1048 elif len(args) == 2:
1049 log_args = [args[0] + '..' + args[1]]
1050 else:
1051 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001052 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053
1054
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055def CMDpresubmit(parser, args):
1056 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001057 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001059 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001060 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 (options, args) = parser.parse_args(args)
1062
ukai@chromium.org259e4682012-10-25 07:36:33 +00001063 if not options.force and is_dirty_git_tree('presubmit'):
1064 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065 return 1
1066
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001067 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 if args:
1069 base_branch = args[0]
1070 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001071 # Default to diffing against the common ancestor of the upstream branch.
1072 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001073
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001074 cl.RunHook(
1075 committing=not options.upload,
1076 may_prompt=False,
1077 verbose=options.verbose,
1078 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001079 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080
1081
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001082def AddChangeIdToCommitMessage(options, args):
1083 """Re-commits using the current message, assumes the commit hook is in
1084 place.
1085 """
1086 log_desc = options.message or CreateDescriptionFromLog(args)
1087 git_command = ['commit', '--amend', '-m', log_desc]
1088 RunGit(git_command)
1089 new_log_desc = CreateDescriptionFromLog(args)
1090 if CHANGE_ID in new_log_desc:
1091 print 'git-cl: Added Change-Id to commit message.'
1092 else:
1093 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1094
1095
ukai@chromium.orge8077812012-02-03 03:41:46 +00001096def GerritUpload(options, args, cl):
1097 """upload the current branch to gerrit."""
1098 # We assume the remote called "origin" is the one we want.
1099 # It is probably not worthwhile to support different workflows.
1100 remote = 'origin'
1101 branch = 'master'
1102 if options.target_branch:
1103 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001105 log_desc = options.message or CreateDescriptionFromLog(args)
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001106 if CHANGE_ID not in log_desc:
1107 AddChangeIdToCommitMessage(options, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001108 if options.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001109 log_desc += '\nR=' + ', '.join(options.reviewers)
1110 change_desc = ChangeDescription(log_desc, ', '.join(options.reviewers))
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001111 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +00001112 if change_desc.IsEmpty():
1113 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114 return 1
1115
ukai@chromium.orge8077812012-02-03 03:41:46 +00001116 receive_options = []
1117 cc = cl.GetCCList().split(',')
1118 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001119 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001120 cc = filter(None, cc)
1121 if cc:
1122 receive_options += ['--cc=' + email for email in cc]
1123 if change_desc.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001124 reviewers = filter(
1125 None, (r.strip() for r in change_desc.reviewers.split(',')))
ukai@chromium.orge8077812012-02-03 03:41:46 +00001126 if reviewers:
1127 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128
ukai@chromium.orge8077812012-02-03 03:41:46 +00001129 git_command = ['push']
1130 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001131 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001132 ' '.join(receive_options))
1133 git_command += [remote, 'HEAD:refs/for/' + branch]
1134 RunGit(git_command)
1135 # TODO(ukai): parse Change-Id: and set issue number?
1136 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138
ukai@chromium.orge8077812012-02-03 03:41:46 +00001139def RietveldUpload(options, args, cl):
1140 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1142 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 if options.emulate_svn_auto_props:
1144 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 change_desc = None
1147
1148 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001149 if options.title:
1150 upload_args.extend(['--title', options.title])
1151 elif options.message:
1152 # TODO(rogerta): for now, the -m option will also set the --title option
1153 # for upload.py. Soon this will be changed to set the --message option.
1154 # Will wait until people are used to typing -t instead of -m.
1155 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001156 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 print ("This branch is associated with issue %s. "
1158 "Adding patch to that issue." % cl.GetIssue())
1159 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001160 if options.title:
1161 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001162 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001163 change_desc = ChangeDescription(message, ','.join(options.reviewers))
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001164 if not options.force:
1165 change_desc.Prompt()
1166 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001167
1168 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 print "Description is empty; aborting."
1170 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001171
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001172 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001173 if change_desc.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001174 upload_args.extend(
1175 [
1176 '--reviewers',
1177 ','.join(r.strip() for r in change_desc.reviewers.split(',')),
1178 ])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001179 if options.send_mail:
1180 if not change_desc.reviewers:
1181 DieWithError("Must specify reviewers to send email.")
1182 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001183 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001184 if cc:
1185 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001187 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001188 if not options.find_copies:
1189 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001190
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191 # Include the upstream repo's URL in the change -- this is useful for
1192 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001193 remote_url = cl.GetGitBaseUrlFromConfig()
1194 if not remote_url:
1195 if settings.GetIsGitSvn():
1196 # URL is dependent on the current directory.
1197 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1198 if data:
1199 keys = dict(line.split(': ', 1) for line in data.splitlines()
1200 if ': ' in line)
1201 remote_url = keys.get('URL', None)
1202 else:
1203 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1204 remote_url = (cl.GetRemoteUrl() + '@'
1205 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 if remote_url:
1207 upload_args.extend(['--base_url', remote_url])
1208
1209 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001210 upload_args = ['upload'] + upload_args + args
1211 logging.info('upload.RealMain(%s)', upload_args)
1212 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001213 except KeyboardInterrupt:
1214 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 except:
1216 # If we got an exception after the user typed a description for their
1217 # change, back up the description before re-raising.
1218 if change_desc:
1219 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1220 print '\nGot exception while uploading -- saving description to %s\n' \
1221 % backup_path
1222 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001223 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 backup_file.close()
1225 raise
1226
1227 if not cl.GetIssue():
1228 cl.SetIssue(issue)
1229 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001230
1231 if options.use_commit_queue:
1232 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 return 0
1234
1235
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001236def cleanup_list(l):
1237 """Fixes a list so that comma separated items are put as individual items.
1238
1239 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1240 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1241 """
1242 items = sum((i.split(',') for i in l), [])
1243 stripped_items = (i.strip() for i in items)
1244 return sorted(filter(None, stripped_items))
1245
1246
ukai@chromium.orge8077812012-02-03 03:41:46 +00001247@usage('[args to "git diff"]')
1248def CMDupload(parser, args):
1249 """upload the current changelist to codereview"""
1250 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1251 help='bypass upload presubmit hook')
1252 parser.add_option('-f', action='store_true', dest='force',
1253 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001254 parser.add_option('-m', dest='message', help='message for patchset')
1255 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001256 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001257 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001258 help='reviewer email addresses')
1259 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001260 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001261 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001262 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001263 help='send email to reviewer immediately')
1264 parser.add_option("--emulate_svn_auto_props", action="store_true",
1265 dest="emulate_svn_auto_props",
1266 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001267 parser.add_option('-c', '--use-commit-queue', action='store_true',
1268 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001269 parser.add_option('--target_branch',
1270 help='When uploading to gerrit, remote branch to '
1271 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001272 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001273 (options, args) = parser.parse_args(args)
1274
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001275 if options.target_branch and not settings.GetIsGerrit():
1276 parser.error('Use --target_branch for non gerrit repository.')
1277
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001278 # Print warning if the user used the -m/--message argument. This will soon
1279 # change to -t/--title.
1280 if options.message:
1281 print >> sys.stderr, (
1282 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1283 'In the near future, -m or --message will send a message instead.\n'
1284 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001285
ukai@chromium.org259e4682012-10-25 07:36:33 +00001286 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001287 return 1
1288
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001289 options.reviewers = cleanup_list(options.reviewers)
1290 options.cc = cleanup_list(options.cc)
1291
ukai@chromium.orge8077812012-02-03 03:41:46 +00001292 cl = Changelist()
1293 if args:
1294 # TODO(ukai): is it ok for gerrit case?
1295 base_branch = args[0]
1296 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001297 # Default to diffing against common ancestor of upstream branch
1298 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001299 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001300
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001301 # Apply watchlists on upload.
1302 change = cl.GetChange(base_branch, None)
1303 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1304 files = [f.LocalPath() for f in change.AffectedFiles()]
1305 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1306
ukai@chromium.orge8077812012-02-03 03:41:46 +00001307 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001308 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001309 may_prompt=not options.force,
1310 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001311 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001312 if not hook_results.should_continue():
1313 return 1
1314 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001315 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001316
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001317 if cl.GetIssue():
1318 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1319 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001320 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001321 print ('The last upload made from this repository was patchset #%d but '
1322 'the most recent patchset on the server is #%d.'
1323 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001324 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1325 'from another machine or branch the patch you\'re uploading now '
1326 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001327 ask_for_data('About to upload; enter to confirm.')
1328
iannucci@chromium.org79540052012-10-19 23:15:26 +00001329 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001330 if settings.GetIsGerrit():
1331 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001332 ret = RietveldUpload(options, args, cl)
1333 if not ret:
1334 git_set_branch_value('last-upload-hash', RunGit(['rev-parse', 'HEAD']))
1335
1336 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001337
1338
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001339def IsSubmoduleMergeCommit(ref):
1340 # When submodules are added to the repo, we expect there to be a single
1341 # non-git-svn merge commit at remote HEAD with a signature comment.
1342 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001343 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001344 return RunGit(cmd) != ''
1345
1346
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347def SendUpstream(parser, args, cmd):
1348 """Common code for CmdPush and CmdDCommit
1349
1350 Squashed commit into a single.
1351 Updates changelog with metadata (e.g. pointer to review).
1352 Pushes/dcommits the code upstream.
1353 Updates review and closes.
1354 """
1355 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1356 help='bypass upload presubmit hook')
1357 parser.add_option('-m', dest='message',
1358 help="override review description")
1359 parser.add_option('-f', action='store_true', dest='force',
1360 help="force yes to questions (don't prompt)")
1361 parser.add_option('-c', dest='contributor',
1362 help="external contributor for patch (appended to " +
1363 "description and used as author for git). Should be " +
1364 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001365 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 (options, args) = parser.parse_args(args)
1367 cl = Changelist()
1368
1369 if not args or cmd == 'push':
1370 # Default to merging against our best guess of the upstream branch.
1371 args = [cl.GetUpstreamBranch()]
1372
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001373 if options.contributor:
1374 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1375 print "Please provide contibutor as 'First Last <email@example.com>'"
1376 return 1
1377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001379 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380
ukai@chromium.org259e4682012-10-25 07:36:33 +00001381 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 return 1
1383
1384 # This rev-list syntax means "show all commits not in my branch that
1385 # are in base_branch".
1386 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1387 base_branch]).splitlines()
1388 if upstream_commits:
1389 print ('Base branch "%s" has %d commits '
1390 'not in this branch.' % (base_branch, len(upstream_commits)))
1391 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1392 return 1
1393
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001394 # This is the revision `svn dcommit` will commit on top of.
1395 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1396 '--pretty=format:%H'])
1397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001399 # If the base_head is a submodule merge commit, the first parent of the
1400 # base_head should be a git-svn commit, which is what we're interested in.
1401 base_svn_head = base_branch
1402 if base_has_submodules:
1403 base_svn_head += '^1'
1404
1405 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406 if extra_commits:
1407 print ('This branch has %d additional commits not upstreamed yet.'
1408 % len(extra_commits.splitlines()))
1409 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1410 'before attempting to %s.' % (base_branch, cmd))
1411 return 1
1412
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001413 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001414 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001415 author = None
1416 if options.contributor:
1417 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001418 hook_results = cl.RunHook(
1419 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001420 may_prompt=not options.force,
1421 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001422 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001423 if not hook_results.should_continue():
1424 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425
1426 if cmd == 'dcommit':
1427 # Check the tree status if the tree status URL is set.
1428 status = GetTreeStatus()
1429 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001430 print('The tree is closed. Please wait for it to reopen. Use '
1431 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432 return 1
1433 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001434 print('Unable to determine tree status. Please verify manually and '
1435 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001436 else:
1437 breakpad.SendStack(
1438 'GitClHooksBypassedCommit',
1439 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001440 (cl.GetRietveldServer(), cl.GetIssue()),
1441 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442
1443 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001444 if not description and cl.GetIssue():
1445 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001447 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001448 if not cl.GetIssue() and options.bypass_hooks:
1449 description = CreateDescriptionFromLog([base_branch])
1450 else:
1451 print 'No description set.'
1452 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1453 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001455 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457
1458 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 description += "\nPatch from %s." % options.contributor
1460 print 'Description:', repr(description)
1461
1462 branches = [base_branch, cl.GetBranchRef()]
1463 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001464 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001465 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001467 # We want to squash all this branch's commits into one commit with the proper
1468 # description. We do this by doing a "reset --soft" to the base branch (which
1469 # keeps the working copy the same), then dcommitting that. If origin/master
1470 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1471 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001473 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1474 # Delete the branches if they exist.
1475 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1476 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1477 result = RunGitWithCode(showref_cmd)
1478 if result[0] == 0:
1479 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480
1481 # We might be in a directory that's present in this branch but not in the
1482 # trunk. Move up to the top of the tree so that git commands that expect a
1483 # valid CWD won't fail after we check out the merge branch.
1484 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1485 if rel_base_path:
1486 os.chdir(rel_base_path)
1487
1488 # Stuff our change into the merge branch.
1489 # We wrap in a try...finally block so if anything goes wrong,
1490 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001491 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001493 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1494 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 if options.contributor:
1496 RunGit(['commit', '--author', options.contributor, '-m', description])
1497 else:
1498 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001499 if base_has_submodules:
1500 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1501 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1502 RunGit(['checkout', CHERRY_PICK_BRANCH])
1503 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 if cmd == 'push':
1505 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001506 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507 retcode, output = RunGitWithCode(
1508 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1509 logging.debug(output)
1510 else:
1511 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001512 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001513 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001514 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515 finally:
1516 # And then swap back to the original branch and clean up.
1517 RunGit(['checkout', '-q', cl.GetBranch()])
1518 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001519 if base_has_submodules:
1520 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521
1522 if cl.GetIssue():
1523 if cmd == 'dcommit' and 'Committed r' in output:
1524 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1525 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001526 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1527 for l in output.splitlines(False))
1528 match = filter(None, match)
1529 if len(match) != 1:
1530 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1531 output)
1532 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 else:
1534 return 1
1535 viewvc_url = settings.GetViewVCUrl()
1536 if viewvc_url and revision:
1537 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001538 elif revision:
1539 cl.description += ('\n\nCommitted: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540 print ('Closing issue '
1541 '(you may be prompted for your codereview password)...')
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001542 cl.UpdateDescription(cl.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001544 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001545 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001546 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001547 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1548 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001549 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001550
1551 if retcode == 0:
1552 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1553 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001554 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001555
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556 return 0
1557
1558
1559@usage('[upstream branch to apply against]')
1560def CMDdcommit(parser, args):
1561 """commit the current changelist via git-svn"""
1562 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001563 message = """This doesn't appear to be an SVN repository.
1564If your project has a git mirror with an upstream SVN master, you probably need
1565to run 'git svn init', see your project's git mirror documentation.
1566If your project has a true writeable upstream repository, you probably want
1567to run 'git cl push' instead.
1568Choose wisely, if you get this wrong, your commit might appear to succeed but
1569will instead be silently ignored."""
1570 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001571 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572 return SendUpstream(parser, args, 'dcommit')
1573
1574
1575@usage('[upstream branch to apply against]')
1576def CMDpush(parser, args):
1577 """commit the current changelist via git"""
1578 if settings.GetIsGitSvn():
1579 print('This appears to be an SVN repository.')
1580 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001581 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582 return SendUpstream(parser, args, 'push')
1583
1584
1585@usage('<patch url or issue id>')
1586def CMDpatch(parser, args):
1587 """patch in a code review"""
1588 parser.add_option('-b', dest='newbranch',
1589 help='create a new branch off trunk for the patch')
1590 parser.add_option('-f', action='store_true', dest='force',
1591 help='with -b, clobber any existing branch')
1592 parser.add_option('--reject', action='store_true', dest='reject',
1593 help='allow failed patches and spew .rej files')
1594 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1595 help="don't commit after patch applies")
1596 (options, args) = parser.parse_args(args)
1597 if len(args) != 1:
1598 parser.print_help()
1599 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001600 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001601
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001602 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001603 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001604
maruel@chromium.org52424302012-08-29 15:14:30 +00001605 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001606 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001607 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001608 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001609 patchset = cl.GetMostRecentPatchset(issue)
1610 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001612 # Assume it's a URL to the patch. Default to https.
1613 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001614 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001615 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616 DieWithError('Must pass an issue ID or full URL for '
1617 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001618 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001619 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001620 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621
1622 if options.newbranch:
1623 if options.force:
1624 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001625 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626 RunGit(['checkout', '-b', options.newbranch,
1627 Changelist().GetUpstreamBranch()])
1628
1629 # Switch up to the top-level directory, if necessary, in preparation for
1630 # applying the patch.
1631 top = RunGit(['rev-parse', '--show-cdup']).strip()
1632 if top:
1633 os.chdir(top)
1634
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001635 # Git patches have a/ at the beginning of source paths. We strip that out
1636 # with a sed script rather than the -p flag to patch so we can feed either
1637 # Git or svn-style patches into the same apply command.
1638 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001639 try:
1640 patch_data = subprocess2.check_output(
1641 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1642 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643 DieWithError('Git patch mungling failed.')
1644 logging.info(patch_data)
1645 # We use "git apply" to apply the patch instead of "patch" so that we can
1646 # pick up file adds.
1647 # The --index flag means: also insert into the index (so we catch adds).
1648 cmd = ['git', 'apply', '--index', '-p0']
1649 if options.reject:
1650 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001651 try:
1652 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1653 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654 DieWithError('Failed to apply the patch')
1655
1656 # If we had an issue, commit the current state and register the issue.
1657 if not options.nocommit:
1658 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1659 cl = Changelist()
1660 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001661 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001662 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663 else:
1664 print "Patch applied to index."
1665 return 0
1666
1667
1668def CMDrebase(parser, args):
1669 """rebase current branch on top of svn repo"""
1670 # Provide a wrapper for git svn rebase to help avoid accidental
1671 # git svn dcommit.
1672 # It's the only command that doesn't use parser at all since we just defer
1673 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001674 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675
1676
1677def GetTreeStatus():
1678 """Fetches the tree status and returns either 'open', 'closed',
1679 'unknown' or 'unset'."""
1680 url = settings.GetTreeStatusUrl(error_ok=True)
1681 if url:
1682 status = urllib2.urlopen(url).read().lower()
1683 if status.find('closed') != -1 or status == '0':
1684 return 'closed'
1685 elif status.find('open') != -1 or status == '1':
1686 return 'open'
1687 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001688 return 'unset'
1689
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691def GetTreeStatusReason():
1692 """Fetches the tree status from a json url and returns the message
1693 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001694 url = settings.GetTreeStatusUrl()
1695 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001696 connection = urllib2.urlopen(json_url)
1697 status = json.loads(connection.read())
1698 connection.close()
1699 return status['message']
1700
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001701
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001702def CMDtree(parser, args):
1703 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001704 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001705 status = GetTreeStatus()
1706 if 'unset' == status:
1707 print 'You must configure your tree status URL by running "git cl config".'
1708 return 2
1709
1710 print "The tree is %s" % status
1711 print
1712 print GetTreeStatusReason()
1713 if status != 'open':
1714 return 1
1715 return 0
1716
1717
maruel@chromium.org15192402012-09-06 12:38:29 +00001718def CMDtry(parser, args):
1719 """Triggers a try job through Rietveld."""
1720 group = optparse.OptionGroup(parser, "Try job options")
1721 group.add_option(
1722 "-b", "--bot", action="append",
1723 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1724 "times to specify multiple builders. ex: "
1725 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1726 "the try server waterfall for the builders name and the tests "
1727 "available. Can also be used to specify gtest_filter, e.g. "
1728 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1729 group.add_option(
1730 "-r", "--revision",
1731 help="Revision to use for the try job; default: the "
1732 "revision will be determined by the try server; see "
1733 "its waterfall for more info")
1734 group.add_option(
1735 "-c", "--clobber", action="store_true", default=False,
1736 help="Force a clobber before building; e.g. don't do an "
1737 "incremental build")
1738 group.add_option(
1739 "--project",
1740 help="Override which project to use. Projects are defined "
1741 "server-side to define what default bot set to use")
1742 group.add_option(
1743 "-t", "--testfilter", action="append", default=[],
1744 help=("Apply a testfilter to all the selected builders. Unless the "
1745 "builders configurations are similar, use multiple "
1746 "--bot <builder>:<test> arguments."))
1747 group.add_option(
1748 "-n", "--name", help="Try job name; default to current branch name")
1749 parser.add_option_group(group)
1750 options, args = parser.parse_args(args)
1751
1752 if args:
1753 parser.error('Unknown arguments: %s' % args)
1754
1755 cl = Changelist()
1756 if not cl.GetIssue():
1757 parser.error('Need to upload first')
1758
1759 if not options.name:
1760 options.name = cl.GetBranch()
1761
1762 # Process --bot and --testfilter.
1763 if not options.bot:
1764 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001765 change = cl.GetChange(
1766 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1767 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001768 options.bot = presubmit_support.DoGetTrySlaves(
1769 change,
1770 change.LocalPaths(),
1771 settings.GetRoot(),
1772 None,
1773 None,
1774 options.verbose,
1775 sys.stdout)
1776 if not options.bot:
1777 parser.error('No default try builder to try, use --bot')
1778
1779 builders_and_tests = {}
1780 for bot in options.bot:
1781 if ':' in bot:
1782 builder, tests = bot.split(':', 1)
1783 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1784 elif ',' in bot:
1785 parser.error('Specify one bot per --bot flag')
1786 else:
1787 builders_and_tests.setdefault(bot, []).append('defaulttests')
1788
1789 if options.testfilter:
1790 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1791 builders_and_tests = dict(
1792 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1793 if t != ['compile'])
1794
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001795 if any('triggered' in b for b in builders_and_tests):
1796 print >> sys.stderr, (
1797 'ERROR You are trying to send a job to a triggered bot. This type of'
1798 ' bot requires an\ninitial job from a parent (usually a builder). '
1799 'Instead send your job to the parent.\n'
1800 'Bot list: %s' % builders_and_tests)
1801 return 1
1802
maruel@chromium.org15192402012-09-06 12:38:29 +00001803 patchset = cl.GetPatchset()
1804 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001805 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001806
1807 cl.RpcServer().trigger_try_jobs(
1808 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1809 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001810 print('Tried jobs on:')
1811 length = max(len(builder) for builder in builders_and_tests)
1812 for builder in sorted(builders_and_tests):
1813 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001814 return 0
1815
1816
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001817@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001819 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001820 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001821 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001822 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001823 return 0
1824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001825 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001826 if args:
1827 # One arg means set upstream branch.
1828 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1829 cl = Changelist()
1830 print "Upstream branch set to " + cl.GetUpstreamBranch()
1831 else:
1832 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 return 0
1834
1835
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001836def CMDset_commit(parser, args):
1837 """set the commit bit"""
1838 _, args = parser.parse_args(args)
1839 if args:
1840 parser.error('Unrecognized args: %s' % ' '.join(args))
1841 cl = Changelist()
1842 cl.SetFlag('commit', '1')
1843 return 0
1844
1845
groby@chromium.org411034a2013-02-26 15:12:01 +00001846def CMDset_close(parser, args):
1847 """close the issue"""
1848 _, args = parser.parse_args(args)
1849 if args:
1850 parser.error('Unrecognized args: %s' % ' '.join(args))
1851 cl = Changelist()
1852 # Ensure there actually is an issue to close.
1853 cl.GetDescription()
1854 cl.CloseIssue()
1855 return 0
1856
1857
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001858def Command(name):
1859 return getattr(sys.modules[__name__], 'CMD' + name, None)
1860
1861
1862def CMDhelp(parser, args):
1863 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001864 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865 if len(args) == 1:
1866 return main(args + ['--help'])
1867 parser.print_help()
1868 return 0
1869
1870
1871def GenUsage(parser, command):
1872 """Modify an OptParse object with the function's documentation."""
1873 obj = Command(command)
1874 more = getattr(obj, 'usage_more', '')
1875 if command == 'help':
1876 command = '<command>'
1877 else:
1878 # OptParser.description prefer nicely non-formatted strings.
1879 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1880 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1881
1882
1883def main(argv):
1884 """Doesn't parse the arguments here, just find the right subcommand to
1885 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001886 if sys.hexversion < 0x02060000:
1887 print >> sys.stderr, (
1888 '\nYour python version %s is unsupported, please upgrade.\n' %
1889 sys.version.split(' ', 1)[0])
1890 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001891 # Reload settings.
1892 global settings
1893 settings = Settings()
1894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001895 # Do it late so all commands are listed.
1896 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1897 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1898 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1899
1900 # Create the option parse and add --verbose support.
1901 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001902 parser.add_option(
1903 '-v', '--verbose', action='count', default=0,
1904 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 old_parser_args = parser.parse_args
1906 def Parse(args):
1907 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001908 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001910 elif options.verbose:
1911 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912 else:
1913 logging.basicConfig(level=logging.WARNING)
1914 return options, args
1915 parser.parse_args = Parse
1916
1917 if argv:
1918 command = Command(argv[0])
1919 if command:
1920 # "fix" the usage and the description now that we know the subcommand.
1921 GenUsage(parser, argv[0])
1922 try:
1923 return command(parser, argv[1:])
1924 except urllib2.HTTPError, e:
1925 if e.code != 500:
1926 raise
1927 DieWithError(
1928 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1929 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1930
1931 # Not a known command. Default to help.
1932 GenUsage(parser, 'help')
1933 return CMDhelp(parser, argv)
1934
1935
1936if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001937 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938 sys.exit(main(sys.argv[1:]))