blob: 64cd9e86f091e00d0201b242187930fd393de70b [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.org78936cb2013-04-11 00:17:52 +000057 except subprocess2.CalledProcessError as e:
58 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061 'Command "%s" failed.\n%s' % (
62 ' '.join(args), error_message or e.stdout or ''))
63 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000064
65
66def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000067 """Returns stdout."""
68 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
70
71def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000073 try:
74 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
75 return code, out[0]
76 except ValueError:
77 # When the subprocess fails, it returns None. That triggers a ValueError
78 # when trying to unpack the return value into (out, code).
79 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def usage(more):
83 def hook(fn):
84 fn.usage_more = more
85 return fn
86 return hook
87
88
maruel@chromium.org90541732011-04-01 17:54:18 +000089def ask_for_data(prompt):
90 try:
91 return raw_input(prompt)
92 except KeyboardInterrupt:
93 # Hide the exception.
94 sys.exit(1)
95
96
iannucci@chromium.org79540052012-10-19 23:15:26 +000097def git_set_branch_value(key, value):
98 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +000099 if not branch:
100 return
101
102 cmd = ['config']
103 if isinstance(value, int):
104 cmd.append('--int')
105 git_key = 'branch.%s.%s' % (branch, key)
106 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000107
108
109def git_get_branch_default(key, default):
110 branch = Changelist().GetBranch()
111 if branch:
112 git_key = 'branch.%s.%s' % (branch, key)
113 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
114 try:
115 return int(stdout.strip())
116 except ValueError:
117 pass
118 return default
119
120
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000121def add_git_similarity(parser):
122 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000123 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000124 help='Sets the percentage that a pair of files need to match in order to'
125 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000126 parser.add_option(
127 '--find-copies', action='store_true',
128 help='Allows git to look for copies.')
129 parser.add_option(
130 '--no-find-copies', action='store_false', dest='find_copies',
131 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000132
133 old_parser_args = parser.parse_args
134 def Parse(args):
135 options, args = old_parser_args(args)
136
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000139 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000140 print('Note: Saving similarity of %d%% in git config.'
141 % options.similarity)
142 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000143
iannucci@chromium.org79540052012-10-19 23:15:26 +0000144 options.similarity = max(0, min(options.similarity, 100))
145
146 if options.find_copies is None:
147 options.find_copies = bool(
148 git_get_branch_default('git-find-copies', True))
149 else:
150 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000151
152 print('Using %d%% similarity for rename/copy detection. '
153 'Override with --similarity.' % options.similarity)
154
155 return options, args
156 parser.parse_args = Parse
157
158
ukai@chromium.org259e4682012-10-25 07:36:33 +0000159def is_dirty_git_tree(cmd):
160 # Make sure index is up-to-date before running diff-index.
161 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
162 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
163 if dirty:
164 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
165 print 'Uncommitted files: (git diff-index --name-status HEAD)'
166 print dirty[:4096]
167 if len(dirty) > 4096:
168 print '... (run "git diff-index --name-status HEAD" to see full output).'
169 return True
170 return False
171
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000172
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000173def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
174 """Return the corresponding git ref if |base_url| together with |glob_spec|
175 matches the full |url|.
176
177 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
178 """
179 fetch_suburl, as_ref = glob_spec.split(':')
180 if allow_wildcards:
181 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
182 if glob_match:
183 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
184 # "branches/{472,597,648}/src:refs/remotes/svn/*".
185 branch_re = re.escape(base_url)
186 if glob_match.group(1):
187 branch_re += '/' + re.escape(glob_match.group(1))
188 wildcard = glob_match.group(2)
189 if wildcard == '*':
190 branch_re += '([^/]*)'
191 else:
192 # Escape and replace surrounding braces with parentheses and commas
193 # with pipe symbols.
194 wildcard = re.escape(wildcard)
195 wildcard = re.sub('^\\\\{', '(', wildcard)
196 wildcard = re.sub('\\\\,', '|', wildcard)
197 wildcard = re.sub('\\\\}$', ')', wildcard)
198 branch_re += wildcard
199 if glob_match.group(3):
200 branch_re += re.escape(glob_match.group(3))
201 match = re.match(branch_re, url)
202 if match:
203 return re.sub('\*$', match.group(1), as_ref)
204
205 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
206 if fetch_suburl:
207 full_url = base_url + '/' + fetch_suburl
208 else:
209 full_url = base_url
210 if full_url == url:
211 return as_ref
212 return None
213
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000214
iannucci@chromium.org79540052012-10-19 23:15:26 +0000215def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000216 """Prints statistics about the change to the user."""
217 # --no-ext-diff is broken in some versions of Git, so try to work around
218 # this by overriding the environment (but there is still a problem if the
219 # git config key "diff.external" is used).
220 env = os.environ.copy()
221 if 'GIT_EXTERNAL_DIFF' in env:
222 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000223
224 if find_copies:
225 similarity_options = ['--find-copies-harder', '-l100000',
226 '-C%s' % similarity]
227 else:
228 similarity_options = ['-M%s' % similarity]
229
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000230 return subprocess2.call(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000231 ['git', 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
232 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000233
234
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000235class Settings(object):
236 def __init__(self):
237 self.default_server = None
238 self.cc = None
239 self.root = None
240 self.is_git_svn = None
241 self.svn_branch = None
242 self.tree_status_url = None
243 self.viewvc_url = None
244 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000245 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000246
247 def LazyUpdateIfNeeded(self):
248 """Updates the settings from a codereview.settings file, if available."""
249 if not self.updated:
250 cr_settings_file = FindCodereviewSettingsFile()
251 if cr_settings_file:
252 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000253 self.updated = True
254 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000255 self.updated = True
256
257 def GetDefaultServerUrl(self, error_ok=False):
258 if not self.default_server:
259 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000260 self.default_server = gclient_utils.UpgradeToHttps(
261 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000262 if error_ok:
263 return self.default_server
264 if not self.default_server:
265 error_message = ('Could not find settings file. You must configure '
266 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000267 self.default_server = gclient_utils.UpgradeToHttps(
268 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000269 return self.default_server
270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000271 def GetRoot(self):
272 if not self.root:
273 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
274 return self.root
275
276 def GetIsGitSvn(self):
277 """Return true if this repo looks like it's using git-svn."""
278 if self.is_git_svn is None:
279 # If you have any "svn-remote.*" config keys, we think you're using svn.
280 self.is_git_svn = RunGitWithCode(
281 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
282 return self.is_git_svn
283
284 def GetSVNBranch(self):
285 if self.svn_branch is None:
286 if not self.GetIsGitSvn():
287 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
288
289 # Try to figure out which remote branch we're based on.
290 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000291 # 1) iterate through our branch history and find the svn URL.
292 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000293
294 # regexp matching the git-svn line that contains the URL.
295 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
296
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000297 # We don't want to go through all of history, so read a line from the
298 # pipe at a time.
299 # The -100 is an arbitrary limit so we don't search forever.
300 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000301 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000302 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000303 for line in proc.stdout:
304 match = git_svn_re.match(line)
305 if match:
306 url = match.group(1)
307 proc.stdout.close() # Cut pipe.
308 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000310 if url:
311 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
312 remotes = RunGit(['config', '--get-regexp',
313 r'^svn-remote\..*\.url']).splitlines()
314 for remote in remotes:
315 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000317 remote = match.group(1)
318 base_url = match.group(2)
319 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000320 ['config', 'svn-remote.%s.fetch' % remote],
321 error_ok=True).strip()
322 if fetch_spec:
323 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
324 if self.svn_branch:
325 break
326 branch_spec = RunGit(
327 ['config', 'svn-remote.%s.branches' % remote],
328 error_ok=True).strip()
329 if branch_spec:
330 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
331 if self.svn_branch:
332 break
333 tag_spec = RunGit(
334 ['config', 'svn-remote.%s.tags' % remote],
335 error_ok=True).strip()
336 if tag_spec:
337 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
338 if self.svn_branch:
339 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000340
341 if not self.svn_branch:
342 DieWithError('Can\'t guess svn branch -- try specifying it on the '
343 'command line')
344
345 return self.svn_branch
346
347 def GetTreeStatusUrl(self, error_ok=False):
348 if not self.tree_status_url:
349 error_message = ('You must configure your tree status URL by running '
350 '"git cl config".')
351 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
352 error_ok=error_ok,
353 error_message=error_message)
354 return self.tree_status_url
355
356 def GetViewVCUrl(self):
357 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000358 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000359 return self.viewvc_url
360
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000361 def GetDefaultCCList(self):
362 return self._GetConfig('rietveld.cc', error_ok=True)
363
ukai@chromium.orge8077812012-02-03 03:41:46 +0000364 def GetIsGerrit(self):
365 """Return true if this repo is assosiated with gerrit code review system."""
366 if self.is_gerrit is None:
367 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
368 return self.is_gerrit
369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000370 def _GetConfig(self, param, **kwargs):
371 self.LazyUpdateIfNeeded()
372 return RunGit(['config', param], **kwargs).strip()
373
374
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000375def ShortBranchName(branch):
376 """Convert a name like 'refs/heads/foo' to just 'foo'."""
377 return branch.replace('refs/heads/', '')
378
379
380class Changelist(object):
381 def __init__(self, branchref=None):
382 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000383 global settings
384 if not settings:
385 # Happens when git_cl.py is used as a utility library.
386 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000387 settings.GetDefaultServerUrl()
388 self.branchref = branchref
389 if self.branchref:
390 self.branch = ShortBranchName(self.branchref)
391 else:
392 self.branch = None
393 self.rietveld_server = None
394 self.upstream_branch = None
395 self.has_issue = False
396 self.issue = None
397 self.has_description = False
398 self.description = None
399 self.has_patchset = False
400 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000401 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000402 self.cc = None
403 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000404 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000405
406 def GetCCList(self):
407 """Return the users cc'd on this CL.
408
409 Return is a string suitable for passing to gcl with the --cc flag.
410 """
411 if self.cc is None:
412 base_cc = settings .GetDefaultCCList()
413 more_cc = ','.join(self.watchers)
414 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
415 return self.cc
416
417 def SetWatchers(self, watchers):
418 """Set the list of email addresses that should be cc'd based on the changed
419 files in this CL.
420 """
421 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000422
423 def GetBranch(self):
424 """Returns the short branch name, e.g. 'master'."""
425 if not self.branch:
426 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
427 self.branch = ShortBranchName(self.branchref)
428 return self.branch
429
430 def GetBranchRef(self):
431 """Returns the full branch name, e.g. 'refs/heads/master'."""
432 self.GetBranch() # Poke the lazy loader.
433 return self.branchref
434
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000435 @staticmethod
436 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437 """Returns a tuple containg remote and remote ref,
438 e.g. 'origin', 'refs/heads/master'
439 """
440 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
442 error_ok=True).strip()
443 if upstream_branch:
444 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
445 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000446 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
447 error_ok=True).strip()
448 if upstream_branch:
449 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000450 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000451 # Fall back on trying a git-svn upstream branch.
452 if settings.GetIsGitSvn():
453 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000454 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000455 # Else, try to guess the origin remote.
456 remote_branches = RunGit(['branch', '-r']).split()
457 if 'origin/master' in remote_branches:
458 # Fall back on origin/master if it exits.
459 remote = 'origin'
460 upstream_branch = 'refs/heads/master'
461 elif 'origin/trunk' in remote_branches:
462 # Fall back on origin/trunk if it exists. Generally a shared
463 # git-svn clone
464 remote = 'origin'
465 upstream_branch = 'refs/heads/trunk'
466 else:
467 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000468Either pass complete "git diff"-style arguments, like
469 git cl upload origin/master
470or verify this branch is set up to track another (via the --track argument to
471"git checkout -b ...").""")
472
473 return remote, upstream_branch
474
475 def GetUpstreamBranch(self):
476 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000477 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 if remote is not '.':
479 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
480 self.upstream_branch = upstream_branch
481 return self.upstream_branch
482
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000483 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000484 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000485 remote, branch = None, self.GetBranch()
486 seen_branches = set()
487 while branch not in seen_branches:
488 seen_branches.add(branch)
489 remote, branch = self.FetchUpstreamTuple(branch)
490 branch = ShortBranchName(branch)
491 if remote != '.' or branch.startswith('refs/remotes'):
492 break
493 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000494 remotes = RunGit(['remote'], error_ok=True).split()
495 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000496 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000497 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000498 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000499 logging.warning('Could not determine which remote this change is '
500 'associated with, so defaulting to "%s". This may '
501 'not be what you want. You may prevent this message '
502 'by running "git svn info" as documented here: %s',
503 self._remote,
504 GIT_INSTRUCTIONS_URL)
505 else:
506 logging.warn('Could not determine which remote this change is '
507 'associated with. You may prevent this message by '
508 'running "git svn info" as documented here: %s',
509 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000510 branch = 'HEAD'
511 if branch.startswith('refs/remotes'):
512 self._remote = (remote, branch)
513 else:
514 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000515 return self._remote
516
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000517 def GitSanityChecks(self, upstream_git_obj):
518 """Checks git repo status and ensures diff is from local commits."""
519
520 # Verify the commit we're diffing against is in our current branch.
521 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
522 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
523 if upstream_sha != common_ancestor:
524 print >> sys.stderr, (
525 'ERROR: %s is not in the current branch. You may need to rebase '
526 'your tracking branch' % upstream_sha)
527 return False
528
529 # List the commits inside the diff, and verify they are all local.
530 commits_in_diff = RunGit(
531 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
532 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
533 remote_branch = remote_branch.strip()
534 if code != 0:
535 _, remote_branch = self.GetRemoteBranch()
536
537 commits_in_remote = RunGit(
538 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
539
540 common_commits = set(commits_in_diff) & set(commits_in_remote)
541 if common_commits:
542 print >> sys.stderr, (
543 'ERROR: Your diff contains %d commits already in %s.\n'
544 'Run "git log --oneline %s..HEAD" to get a list of commits in '
545 'the diff. If you are using a custom git flow, you can override'
546 ' the reference used for this check with "git config '
547 'gitcl.remotebranch <git-ref>".' % (
548 len(common_commits), remote_branch, upstream_git_obj))
549 return False
550 return True
551
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000552 def GetGitBaseUrlFromConfig(self):
553 """Return the configured base URL from branch.<branchname>.baseurl.
554
555 Returns None if it is not set.
556 """
557 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
558 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000559
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000560 def GetRemoteUrl(self):
561 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
562
563 Returns None if there is no remote.
564 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000565 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000566 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
567
568 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000569 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
572 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000573 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574 else:
575 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000576 self.has_issue = True
577 return self.issue
578
579 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000580 if not self.rietveld_server:
581 # If we're on a branch then get the server potentially associated
582 # with that branch.
583 if self.GetIssue():
584 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
585 ['config', self._RietveldServer()], error_ok=True).strip())
586 if not self.rietveld_server:
587 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 return self.rietveld_server
589
590 def GetIssueURL(self):
591 """Get the URL for a particular issue."""
592 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
593
594 def GetDescription(self, pretty=False):
595 if not self.has_description:
596 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000597 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000598 try:
599 self.description = self.RpcServer().get_description(issue).strip()
600 except urllib2.HTTPError, e:
601 if e.code == 404:
602 DieWithError(
603 ('\nWhile fetching the description for issue %d, received a '
604 '404 (not found)\n'
605 'error. It is likely that you deleted this '
606 'issue on the server. If this is the\n'
607 'case, please run\n\n'
608 ' git cl issue 0\n\n'
609 'to clear the association with the deleted issue. Then run '
610 'this command again.') % issue)
611 else:
612 DieWithError(
613 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 self.has_description = True
615 if pretty:
616 wrapper = textwrap.TextWrapper()
617 wrapper.initial_indent = wrapper.subsequent_indent = ' '
618 return wrapper.fill(self.description)
619 return self.description
620
621 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000622 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 if not self.has_patchset:
624 patchset = RunGit(['config', self._PatchsetSetting()],
625 error_ok=True).strip()
626 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000627 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 else:
629 self.patchset = None
630 self.has_patchset = True
631 return self.patchset
632
633 def SetPatchset(self, patchset):
634 """Set this branch's patchset. If patchset=0, clears the patchset."""
635 if patchset:
636 RunGit(['config', self._PatchsetSetting(), str(patchset)])
637 else:
638 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000639 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640 self.has_patchset = False
641
binji@chromium.org0281f522012-09-14 13:37:59 +0000642 def GetMostRecentPatchset(self, issue):
643 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000644 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000645
646 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000647 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000648 '/download/issue%s_%s.diff' % (issue, patchset))
649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 def SetIssue(self, issue):
651 """Set this branch's issue. If issue=0, clears the issue."""
652 if issue:
653 RunGit(['config', self._IssueSetting(), str(issue)])
654 if self.rietveld_server:
655 RunGit(['config', self._RietveldServer(), self.rietveld_server])
656 else:
657 RunGit(['config', '--unset', self._IssueSetting()])
658 self.SetPatchset(0)
659 self.has_issue = False
660
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000661 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000662 if not self.GitSanityChecks(upstream_branch):
663 DieWithError('\nGit sanity check failure')
664
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000665 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
666 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000667
668 # We use the sha1 of HEAD as a name of this change.
669 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000670 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000671 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000672 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000673 except subprocess2.CalledProcessError:
674 DieWithError(
675 ('\nFailed to diff against upstream branch %s!\n\n'
676 'This branch probably doesn\'t exist anymore. To reset the\n'
677 'tracking branch, please run\n'
678 ' git branch --set-upstream %s trunk\n'
679 'replacing trunk with origin/master or the relevant branch') %
680 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000681
maruel@chromium.org52424302012-08-29 15:14:30 +0000682 issue = self.GetIssue()
683 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000684 if issue:
685 description = self.GetDescription()
686 else:
687 # If the change was never uploaded, use the log messages of all commits
688 # up to the branch point, as git cl upload will prefill the description
689 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000690 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
691 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000692
693 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000694 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000695 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000696 name,
697 description,
698 absroot,
699 files,
700 issue,
701 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000702 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000703
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000704 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000705 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000706
707 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000708 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000709 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000710 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000711 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000712 except presubmit_support.PresubmitFailure, e:
713 DieWithError(
714 ('%s\nMaybe your depot_tools is out of date?\n'
715 'If all fails, contact maruel@') % e)
716
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000717 def UpdateDescription(self, description):
718 self.description = description
719 return self.RpcServer().update_description(
720 self.GetIssue(), self.description)
721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000723 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000724 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000726 def SetFlag(self, flag, value):
727 """Patchset must match."""
728 if not self.GetPatchset():
729 DieWithError('The patchset needs to match. Send another patchset.')
730 try:
731 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000732 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000733 except urllib2.HTTPError, e:
734 if e.code == 404:
735 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
736 if e.code == 403:
737 DieWithError(
738 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
739 'match?') % (self.GetIssue(), self.GetPatchset()))
740 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000742 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 """Returns an upload.RpcServer() to access this review's rietveld instance.
744 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000745 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000746 self._rpc_server = rietveld.CachingRietveld(
747 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000748 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000749
750 def _IssueSetting(self):
751 """Return the git setting that stores this change's issue."""
752 return 'branch.%s.rietveldissue' % self.GetBranch()
753
754 def _PatchsetSetting(self):
755 """Return the git setting that stores this change's most recent patchset."""
756 return 'branch.%s.rietveldpatchset' % self.GetBranch()
757
758 def _RietveldServer(self):
759 """Returns the git setting that stores this change's rietveld server."""
760 return 'branch.%s.rietveldserver' % self.GetBranch()
761
762
763def GetCodereviewSettingsInteractively():
764 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000765 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 server = settings.GetDefaultServerUrl(error_ok=True)
767 prompt = 'Rietveld server (host[:port])'
768 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000769 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 if not server and not newserver:
771 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000772 if newserver:
773 newserver = gclient_utils.UpgradeToHttps(newserver)
774 if newserver != server:
775 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000777 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 prompt = caption
779 if initial:
780 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000781 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 if new_val == 'x':
783 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000784 elif new_val:
785 if is_url:
786 new_val = gclient_utils.UpgradeToHttps(new_val)
787 if new_val != initial:
788 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000790 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000792 'tree-status-url', False)
793 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794
795 # TODO: configure a default branch to diff against, rather than this
796 # svn-based hackery.
797
798
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000799class ChangeDescription(object):
800 """Contains a parsed form of the change description."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000801 R_LINE = r'^\s*(TBR|R)\s*=\s*(.+)\s*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000802
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000803 def __init__(self, description):
804 self._description = (description or '').strip()
805
806 @property
807 def description(self):
808 return self._description
809
810 def update_reviewers(self, reviewers):
811 """Rewrites the R=/TBR= line(s) as a single line."""
812 assert isinstance(reviewers, list), reviewers
813 if not reviewers:
814 return
815 regexp = re.compile(self.R_LINE, re.MULTILINE)
816 matches = list(regexp.finditer(self._description))
817 is_tbr = any(m.group(1) == 'TBR' for m in matches)
818 if len(matches) > 1:
819 # Erase all except the first one.
820 for i in xrange(len(matches) - 1, 0, -1):
821 self._description = (
822 self._description[:matches[i].start()] +
823 self._description[matches[i].end()+1:])
824
825 if is_tbr:
826 new_r_line = 'TBR=' + ', '.join(reviewers)
827 else:
828 new_r_line = 'R=' + ', '.join(reviewers)
829
830 if matches:
831 self._description = (
832 self._description[:matches[0].start()] + new_r_line +
833 self._description[matches[0].end()+1:])
834 else:
835 self.append_footer(new_r_line)
836
837 def prompt(self):
838 """Asks the user to update the description."""
839 self._description = (
840 '# Enter a description of the change.\n'
841 '# This will displayed on the codereview site.\n'
842 '# The first line will also be used as the subject of the review.\n'
843 ) + self._description
844
845 if '\nBUG=' not in self._description:
846 self.append_footer('BUG=')
847 content = gclient_utils.RunEditor(self._description, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000848 if not content:
849 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000850
851 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000852 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000853 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000854 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000855 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000856
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000857 def append_footer(self, line):
858 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
859 if self._description:
860 if '\n' not in self._description:
861 self._description += '\n'
862 else:
863 last_line = self._description.rsplit('\n', 1)[1]
864 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
865 not presubmit_support.Change.TAG_LINE_RE.match(line)):
866 self._description += '\n'
867 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000868
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000869 def get_reviewers(self):
870 """Retrieves the list of reviewers."""
871 regexp = re.compile(self.R_LINE, re.MULTILINE)
872 reviewers = [i.group(2) for i in regexp.finditer(self._description)]
873 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000874
875
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876def FindCodereviewSettingsFile(filename='codereview.settings'):
877 """Finds the given file starting in the cwd and going up.
878
879 Only looks up to the top of the repository unless an
880 'inherit-review-settings-ok' file exists in the root of the repository.
881 """
882 inherit_ok_file = 'inherit-review-settings-ok'
883 cwd = os.getcwd()
884 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
885 if os.path.isfile(os.path.join(root, inherit_ok_file)):
886 root = '/'
887 while True:
888 if filename in os.listdir(cwd):
889 if os.path.isfile(os.path.join(cwd, filename)):
890 return open(os.path.join(cwd, filename))
891 if cwd == root:
892 break
893 cwd = os.path.dirname(cwd)
894
895
896def LoadCodereviewSettingsFromFile(fileobj):
897 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000898 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900 def SetProperty(name, setting, unset_error_ok=False):
901 fullname = 'rietveld.' + name
902 if setting in keyvals:
903 RunGit(['config', fullname, keyvals[setting]])
904 else:
905 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
906
907 SetProperty('server', 'CODE_REVIEW_SERVER')
908 # Only server setting is required. Other settings can be absent.
909 # In that case, we ignore errors raised during option deletion attempt.
910 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
911 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
912 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
913
ukai@chromium.orge8077812012-02-03 03:41:46 +0000914 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
915 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
916 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000917
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000918 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
919 #should be of the form
920 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
921 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
922 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
923 keyvals['ORIGIN_URL_CONFIG']])
924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000926def urlretrieve(source, destination):
927 """urllib is broken for SSL connections via a proxy therefore we
928 can't use urllib.urlretrieve()."""
929 with open(destination, 'w') as f:
930 f.write(urllib2.urlopen(source).read())
931
932
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000933def DownloadHooks(force):
934 """downloads hooks
935
936 Args:
937 force: True to update hooks. False to install hooks if not present.
938 """
939 if not settings.GetIsGerrit():
940 return
941 server_url = settings.GetDefaultServerUrl()
942 src = '%s/tools/hooks/commit-msg' % server_url
943 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
944 if not os.access(dst, os.X_OK):
945 if os.path.exists(dst):
946 if not force:
947 return
948 os.remove(dst)
949 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000950 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000951 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
952 except Exception:
953 if os.path.exists(dst):
954 os.remove(dst)
955 DieWithError('\nFailed to download hooks from %s' % src)
956
957
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958@usage('[repo root containing codereview.settings]')
959def CMDconfig(parser, args):
960 """edit configuration for this tree"""
961
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000962 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963 if len(args) == 0:
964 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000965 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000966 return 0
967
968 url = args[0]
969 if not url.endswith('codereview.settings'):
970 url = os.path.join(url, 'codereview.settings')
971
972 # Load code review settings and download hooks (if available).
973 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000974 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975 return 0
976
977
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000978def CMDbaseurl(parser, args):
979 """get or set base-url for this branch"""
980 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
981 branch = ShortBranchName(branchref)
982 _, args = parser.parse_args(args)
983 if not args:
984 print("Current base-url:")
985 return RunGit(['config', 'branch.%s.base-url' % branch],
986 error_ok=False).strip()
987 else:
988 print("Setting base-url to %s" % args[0])
989 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
990 error_ok=False).strip()
991
992
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993def CMDstatus(parser, args):
994 """show status of changelists"""
995 parser.add_option('--field',
996 help='print only specific field (desc|id|patch|url)')
997 (options, args) = parser.parse_args(args)
998
999 # TODO: maybe make show_branches a flag if necessary.
1000 show_branches = not options.field
1001
1002 if show_branches:
1003 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1004 if branches:
1005 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001006 changes = (Changelist(branchref=b) for b in branches.splitlines())
1007 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1008 alignment = max(5, max(len(b) for b in branches))
1009 for branch in sorted(branches):
1010 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011
1012 cl = Changelist()
1013 if options.field:
1014 if options.field.startswith('desc'):
1015 print cl.GetDescription()
1016 elif options.field == 'id':
1017 issueid = cl.GetIssue()
1018 if issueid:
1019 print issueid
1020 elif options.field == 'patch':
1021 patchset = cl.GetPatchset()
1022 if patchset:
1023 print patchset
1024 elif options.field == 'url':
1025 url = cl.GetIssueURL()
1026 if url:
1027 print url
1028 else:
1029 print
1030 print 'Current branch:',
1031 if not cl.GetIssue():
1032 print 'no issue assigned.'
1033 return 0
1034 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001035 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 print 'Issue description:'
1037 print cl.GetDescription(pretty=True)
1038 return 0
1039
1040
1041@usage('[issue_number]')
1042def CMDissue(parser, args):
1043 """Set or display the current code review issue number.
1044
1045 Pass issue number 0 to clear the current issue.
1046"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001047 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048
1049 cl = Changelist()
1050 if len(args) > 0:
1051 try:
1052 issue = int(args[0])
1053 except ValueError:
1054 DieWithError('Pass a number to set the issue or none to list it.\n'
1055 'Maybe you want to run git cl status?')
1056 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001057 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 return 0
1059
1060
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001061def CMDcomments(parser, args):
1062 """show review comments of the current changelist"""
1063 (_, args) = parser.parse_args(args)
1064 if args:
1065 parser.error('Unsupported argument: %s' % args)
1066
1067 cl = Changelist()
1068 if cl.GetIssue():
1069 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1070 for message in sorted(data['messages'], key=lambda x: x['date']):
1071 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1072 if message['text'].strip():
1073 print '\n'.join(' ' + l for l in message['text'].splitlines())
1074 return 0
1075
1076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077def CreateDescriptionFromLog(args):
1078 """Pulls out the commit log to use as a base for the CL description."""
1079 log_args = []
1080 if len(args) == 1 and not args[0].endswith('.'):
1081 log_args = [args[0] + '..']
1082 elif len(args) == 1 and args[0].endswith('...'):
1083 log_args = [args[0][:-1]]
1084 elif len(args) == 2:
1085 log_args = [args[0] + '..' + args[1]]
1086 else:
1087 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001088 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089
1090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091def CMDpresubmit(parser, args):
1092 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001093 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001095 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001096 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 (options, args) = parser.parse_args(args)
1098
ukai@chromium.org259e4682012-10-25 07:36:33 +00001099 if not options.force and is_dirty_git_tree('presubmit'):
1100 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 return 1
1102
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001103 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 if args:
1105 base_branch = args[0]
1106 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 # Default to diffing against the common ancestor of the upstream branch.
1108 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001110 cl.RunHook(
1111 committing=not options.upload,
1112 may_prompt=False,
1113 verbose=options.verbose,
1114 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001115 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116
1117
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001118def AddChangeIdToCommitMessage(options, args):
1119 """Re-commits using the current message, assumes the commit hook is in
1120 place.
1121 """
1122 log_desc = options.message or CreateDescriptionFromLog(args)
1123 git_command = ['commit', '--amend', '-m', log_desc]
1124 RunGit(git_command)
1125 new_log_desc = CreateDescriptionFromLog(args)
1126 if CHANGE_ID in new_log_desc:
1127 print 'git-cl: Added Change-Id to commit message.'
1128 else:
1129 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1130
1131
ukai@chromium.orge8077812012-02-03 03:41:46 +00001132def GerritUpload(options, args, cl):
1133 """upload the current branch to gerrit."""
1134 # We assume the remote called "origin" is the one we want.
1135 # It is probably not worthwhile to support different workflows.
1136 remote = 'origin'
1137 branch = 'master'
1138 if options.target_branch:
1139 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001141 change_desc = ChangeDescription(
1142 options.message or CreateDescriptionFromLog(args))
1143 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001144 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001146 if CHANGE_ID not in change_desc.description:
1147 AddChangeIdToCommitMessage(options, args)
1148 if options.reviewers:
1149 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150
ukai@chromium.orge8077812012-02-03 03:41:46 +00001151 receive_options = []
1152 cc = cl.GetCCList().split(',')
1153 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001154 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001155 cc = filter(None, cc)
1156 if cc:
1157 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001158 if change_desc.get_reviewers():
1159 receive_options.extend(
1160 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161
ukai@chromium.orge8077812012-02-03 03:41:46 +00001162 git_command = ['push']
1163 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001164 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001165 ' '.join(receive_options))
1166 git_command += [remote, 'HEAD:refs/for/' + branch]
1167 RunGit(git_command)
1168 # TODO(ukai): parse Change-Id: and set issue number?
1169 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001170
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171
ukai@chromium.orge8077812012-02-03 03:41:46 +00001172def RietveldUpload(options, args, cl):
1173 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1175 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 if options.emulate_svn_auto_props:
1177 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178
1179 change_desc = None
1180
1181 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001182 if options.title:
1183 upload_args.extend(['--title', options.title])
1184 elif options.message:
1185 # TODO(rogerta): for now, the -m option will also set the --title option
1186 # for upload.py. Soon this will be changed to set the --message option.
1187 # Will wait until people are used to typing -t instead of -m.
1188 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001189 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 print ("This branch is associated with issue %s. "
1191 "Adding patch to that issue." % cl.GetIssue())
1192 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001193 if options.title:
1194 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001195 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001196 change_desc = ChangeDescription(message)
1197 if options.reviewers:
1198 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001199 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001200 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001201
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001202 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 print "Description is empty; aborting."
1204 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001205
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001206 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001207 if change_desc.get_reviewers():
1208 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001209 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001210 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001211 DieWithError("Must specify reviewers to send email.")
1212 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001213 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001214 if cc:
1215 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001217 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001218 if not options.find_copies:
1219 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 # Include the upstream repo's URL in the change -- this is useful for
1222 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001223 remote_url = cl.GetGitBaseUrlFromConfig()
1224 if not remote_url:
1225 if settings.GetIsGitSvn():
1226 # URL is dependent on the current directory.
1227 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1228 if data:
1229 keys = dict(line.split(': ', 1) for line in data.splitlines()
1230 if ': ' in line)
1231 remote_url = keys.get('URL', None)
1232 else:
1233 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1234 remote_url = (cl.GetRemoteUrl() + '@'
1235 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 if remote_url:
1237 upload_args.extend(['--base_url', remote_url])
1238
1239 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001240 upload_args = ['upload'] + upload_args + args
1241 logging.info('upload.RealMain(%s)', upload_args)
1242 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001243 except KeyboardInterrupt:
1244 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 except:
1246 # If we got an exception after the user typed a description for their
1247 # change, back up the description before re-raising.
1248 if change_desc:
1249 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1250 print '\nGot exception while uploading -- saving description to %s\n' \
1251 % backup_path
1252 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001253 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 backup_file.close()
1255 raise
1256
1257 if not cl.GetIssue():
1258 cl.SetIssue(issue)
1259 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001260
1261 if options.use_commit_queue:
1262 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 return 0
1264
1265
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001266def cleanup_list(l):
1267 """Fixes a list so that comma separated items are put as individual items.
1268
1269 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1270 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1271 """
1272 items = sum((i.split(',') for i in l), [])
1273 stripped_items = (i.strip() for i in items)
1274 return sorted(filter(None, stripped_items))
1275
1276
ukai@chromium.orge8077812012-02-03 03:41:46 +00001277@usage('[args to "git diff"]')
1278def CMDupload(parser, args):
1279 """upload the current changelist to codereview"""
1280 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1281 help='bypass upload presubmit hook')
1282 parser.add_option('-f', action='store_true', dest='force',
1283 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001284 parser.add_option('-m', dest='message', help='message for patchset')
1285 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001286 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001287 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001288 help='reviewer email addresses')
1289 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001290 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001291 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001292 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001293 help='send email to reviewer immediately')
1294 parser.add_option("--emulate_svn_auto_props", action="store_true",
1295 dest="emulate_svn_auto_props",
1296 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001297 parser.add_option('-c', '--use-commit-queue', action='store_true',
1298 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001299 parser.add_option('--target_branch',
1300 help='When uploading to gerrit, remote branch to '
1301 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001302 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001303 (options, args) = parser.parse_args(args)
1304
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001305 if options.target_branch and not settings.GetIsGerrit():
1306 parser.error('Use --target_branch for non gerrit repository.')
1307
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001308 # Print warning if the user used the -m/--message argument. This will soon
1309 # change to -t/--title.
1310 if options.message:
1311 print >> sys.stderr, (
1312 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1313 'In the near future, -m or --message will send a message instead.\n'
1314 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001315
ukai@chromium.org259e4682012-10-25 07:36:33 +00001316 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001317 return 1
1318
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001319 options.reviewers = cleanup_list(options.reviewers)
1320 options.cc = cleanup_list(options.cc)
1321
ukai@chromium.orge8077812012-02-03 03:41:46 +00001322 cl = Changelist()
1323 if args:
1324 # TODO(ukai): is it ok for gerrit case?
1325 base_branch = args[0]
1326 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 # Default to diffing against common ancestor of upstream branch
1328 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001329 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001330
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001331 # Apply watchlists on upload.
1332 change = cl.GetChange(base_branch, None)
1333 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1334 files = [f.LocalPath() for f in change.AffectedFiles()]
1335 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1336
ukai@chromium.orge8077812012-02-03 03:41:46 +00001337 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001338 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001339 may_prompt=not options.force,
1340 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001341 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001342 if not hook_results.should_continue():
1343 return 1
1344 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001345 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001346
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001347 if cl.GetIssue():
1348 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1349 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001350 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001351 print ('The last upload made from this repository was patchset #%d but '
1352 'the most recent patchset on the server is #%d.'
1353 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001354 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1355 'from another machine or branch the patch you\'re uploading now '
1356 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001357 ask_for_data('About to upload; enter to confirm.')
1358
iannucci@chromium.org79540052012-10-19 23:15:26 +00001359 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001360 if settings.GetIsGerrit():
1361 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001362 ret = RietveldUpload(options, args, cl)
1363 if not ret:
1364 git_set_branch_value('last-upload-hash', RunGit(['rev-parse', 'HEAD']))
1365
1366 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001367
1368
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001369def IsSubmoduleMergeCommit(ref):
1370 # When submodules are added to the repo, we expect there to be a single
1371 # non-git-svn merge commit at remote HEAD with a signature comment.
1372 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001373 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001374 return RunGit(cmd) != ''
1375
1376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377def SendUpstream(parser, args, cmd):
1378 """Common code for CmdPush and CmdDCommit
1379
1380 Squashed commit into a single.
1381 Updates changelog with metadata (e.g. pointer to review).
1382 Pushes/dcommits the code upstream.
1383 Updates review and closes.
1384 """
1385 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1386 help='bypass upload presubmit hook')
1387 parser.add_option('-m', dest='message',
1388 help="override review description")
1389 parser.add_option('-f', action='store_true', dest='force',
1390 help="force yes to questions (don't prompt)")
1391 parser.add_option('-c', dest='contributor',
1392 help="external contributor for patch (appended to " +
1393 "description and used as author for git). Should be " +
1394 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001395 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 (options, args) = parser.parse_args(args)
1397 cl = Changelist()
1398
1399 if not args or cmd == 'push':
1400 # Default to merging against our best guess of the upstream branch.
1401 args = [cl.GetUpstreamBranch()]
1402
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001403 if options.contributor:
1404 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1405 print "Please provide contibutor as 'First Last <email@example.com>'"
1406 return 1
1407
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001409 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410
ukai@chromium.org259e4682012-10-25 07:36:33 +00001411 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 return 1
1413
1414 # This rev-list syntax means "show all commits not in my branch that
1415 # are in base_branch".
1416 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1417 base_branch]).splitlines()
1418 if upstream_commits:
1419 print ('Base branch "%s" has %d commits '
1420 'not in this branch.' % (base_branch, len(upstream_commits)))
1421 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1422 return 1
1423
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001424 # This is the revision `svn dcommit` will commit on top of.
1425 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1426 '--pretty=format:%H'])
1427
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001429 # If the base_head is a submodule merge commit, the first parent of the
1430 # base_head should be a git-svn commit, which is what we're interested in.
1431 base_svn_head = base_branch
1432 if base_has_submodules:
1433 base_svn_head += '^1'
1434
1435 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 if extra_commits:
1437 print ('This branch has %d additional commits not upstreamed yet.'
1438 % len(extra_commits.splitlines()))
1439 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1440 'before attempting to %s.' % (base_branch, cmd))
1441 return 1
1442
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001443 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001444 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001445 author = None
1446 if options.contributor:
1447 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001448 hook_results = cl.RunHook(
1449 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001450 may_prompt=not options.force,
1451 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001452 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001453 if not hook_results.should_continue():
1454 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455
1456 if cmd == 'dcommit':
1457 # Check the tree status if the tree status URL is set.
1458 status = GetTreeStatus()
1459 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001460 print('The tree is closed. Please wait for it to reopen. Use '
1461 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 return 1
1463 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001464 print('Unable to determine tree status. Please verify manually and '
1465 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001466 else:
1467 breakpad.SendStack(
1468 'GitClHooksBypassedCommit',
1469 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001470 (cl.GetRietveldServer(), cl.GetIssue()),
1471 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 change_desc = ChangeDescription(options.message)
1474 if not change_desc.description and cl.GetIssue():
1475 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001477 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001478 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001479 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001480 else:
1481 print 'No description set.'
1482 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1483 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001484
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001485 # Keep a separate copy for the commit message, because the commit message
1486 # contains the link to the Rietveld issue, while the Rietveld message contains
1487 # the commit viewvc url.
1488 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001489 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001490 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001492 commit_desc.append_footer('Patch from %s.' % options.contributor)
1493
1494 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495
1496 branches = [base_branch, cl.GetBranchRef()]
1497 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001498 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001499 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001501 # We want to squash all this branch's commits into one commit with the proper
1502 # description. We do this by doing a "reset --soft" to the base branch (which
1503 # keeps the working copy the same), then dcommitting that. If origin/master
1504 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1505 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001507 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1508 # Delete the branches if they exist.
1509 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1510 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1511 result = RunGitWithCode(showref_cmd)
1512 if result[0] == 0:
1513 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001514
1515 # We might be in a directory that's present in this branch but not in the
1516 # trunk. Move up to the top of the tree so that git commands that expect a
1517 # valid CWD won't fail after we check out the merge branch.
1518 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1519 if rel_base_path:
1520 os.chdir(rel_base_path)
1521
1522 # Stuff our change into the merge branch.
1523 # We wrap in a try...finally block so if anything goes wrong,
1524 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001525 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001527 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1528 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001530 RunGit(
1531 [
1532 'commit', '--author', options.contributor,
1533 '-m', commit_desc.description,
1534 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001535 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001536 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001537 if base_has_submodules:
1538 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1539 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1540 RunGit(['checkout', CHERRY_PICK_BRANCH])
1541 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542 if cmd == 'push':
1543 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001544 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 retcode, output = RunGitWithCode(
1546 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1547 logging.debug(output)
1548 else:
1549 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001550 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001551 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001552 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001553 finally:
1554 # And then swap back to the original branch and clean up.
1555 RunGit(['checkout', '-q', cl.GetBranch()])
1556 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001557 if base_has_submodules:
1558 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559
1560 if cl.GetIssue():
1561 if cmd == 'dcommit' and 'Committed r' in output:
1562 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1563 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001564 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1565 for l in output.splitlines(False))
1566 match = filter(None, match)
1567 if len(match) != 1:
1568 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1569 output)
1570 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571 else:
1572 return 1
1573 viewvc_url = settings.GetViewVCUrl()
1574 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001575 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001576 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001577 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578 print ('Closing issue '
1579 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001580 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001581 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001582 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001583 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001584 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001585 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1586 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001588
1589 if retcode == 0:
1590 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1591 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001592 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001593
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001594 return 0
1595
1596
1597@usage('[upstream branch to apply against]')
1598def CMDdcommit(parser, args):
1599 """commit the current changelist via git-svn"""
1600 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001601 message = """This doesn't appear to be an SVN repository.
1602If your project has a git mirror with an upstream SVN master, you probably need
1603to run 'git svn init', see your project's git mirror documentation.
1604If your project has a true writeable upstream repository, you probably want
1605to run 'git cl push' instead.
1606Choose wisely, if you get this wrong, your commit might appear to succeed but
1607will instead be silently ignored."""
1608 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001609 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610 return SendUpstream(parser, args, 'dcommit')
1611
1612
1613@usage('[upstream branch to apply against]')
1614def CMDpush(parser, args):
1615 """commit the current changelist via git"""
1616 if settings.GetIsGitSvn():
1617 print('This appears to be an SVN repository.')
1618 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001619 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001620 return SendUpstream(parser, args, 'push')
1621
1622
1623@usage('<patch url or issue id>')
1624def CMDpatch(parser, args):
1625 """patch in a code review"""
1626 parser.add_option('-b', dest='newbranch',
1627 help='create a new branch off trunk for the patch')
1628 parser.add_option('-f', action='store_true', dest='force',
1629 help='with -b, clobber any existing branch')
1630 parser.add_option('--reject', action='store_true', dest='reject',
1631 help='allow failed patches and spew .rej files')
1632 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1633 help="don't commit after patch applies")
1634 (options, args) = parser.parse_args(args)
1635 if len(args) != 1:
1636 parser.print_help()
1637 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001638 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001640 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001641 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001642
maruel@chromium.org52424302012-08-29 15:14:30 +00001643 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001644 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001645 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001646 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001647 patchset = cl.GetMostRecentPatchset(issue)
1648 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001650 # Assume it's a URL to the patch. Default to https.
1651 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001652 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001653 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654 DieWithError('Must pass an issue ID or full URL for '
1655 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001656 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001657 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001658 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001659
1660 if options.newbranch:
1661 if options.force:
1662 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001663 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001664 RunGit(['checkout', '-b', options.newbranch,
1665 Changelist().GetUpstreamBranch()])
1666
1667 # Switch up to the top-level directory, if necessary, in preparation for
1668 # applying the patch.
1669 top = RunGit(['rev-parse', '--show-cdup']).strip()
1670 if top:
1671 os.chdir(top)
1672
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673 # Git patches have a/ at the beginning of source paths. We strip that out
1674 # with a sed script rather than the -p flag to patch so we can feed either
1675 # Git or svn-style patches into the same apply command.
1676 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001677 try:
1678 patch_data = subprocess2.check_output(
1679 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1680 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681 DieWithError('Git patch mungling failed.')
1682 logging.info(patch_data)
1683 # We use "git apply" to apply the patch instead of "patch" so that we can
1684 # pick up file adds.
1685 # The --index flag means: also insert into the index (so we catch adds).
1686 cmd = ['git', 'apply', '--index', '-p0']
1687 if options.reject:
1688 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001689 try:
1690 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1691 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692 DieWithError('Failed to apply the patch')
1693
1694 # If we had an issue, commit the current state and register the issue.
1695 if not options.nocommit:
1696 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1697 cl = Changelist()
1698 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001699 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001700 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701 else:
1702 print "Patch applied to index."
1703 return 0
1704
1705
1706def CMDrebase(parser, args):
1707 """rebase current branch on top of svn repo"""
1708 # Provide a wrapper for git svn rebase to help avoid accidental
1709 # git svn dcommit.
1710 # It's the only command that doesn't use parser at all since we just defer
1711 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001712 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713
1714
1715def GetTreeStatus():
1716 """Fetches the tree status and returns either 'open', 'closed',
1717 'unknown' or 'unset'."""
1718 url = settings.GetTreeStatusUrl(error_ok=True)
1719 if url:
1720 status = urllib2.urlopen(url).read().lower()
1721 if status.find('closed') != -1 or status == '0':
1722 return 'closed'
1723 elif status.find('open') != -1 or status == '1':
1724 return 'open'
1725 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 return 'unset'
1727
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001728
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001729def GetTreeStatusReason():
1730 """Fetches the tree status from a json url and returns the message
1731 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001732 url = settings.GetTreeStatusUrl()
1733 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001734 connection = urllib2.urlopen(json_url)
1735 status = json.loads(connection.read())
1736 connection.close()
1737 return status['message']
1738
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740def CMDtree(parser, args):
1741 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001742 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 status = GetTreeStatus()
1744 if 'unset' == status:
1745 print 'You must configure your tree status URL by running "git cl config".'
1746 return 2
1747
1748 print "The tree is %s" % status
1749 print
1750 print GetTreeStatusReason()
1751 if status != 'open':
1752 return 1
1753 return 0
1754
1755
maruel@chromium.org15192402012-09-06 12:38:29 +00001756def CMDtry(parser, args):
1757 """Triggers a try job through Rietveld."""
1758 group = optparse.OptionGroup(parser, "Try job options")
1759 group.add_option(
1760 "-b", "--bot", action="append",
1761 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1762 "times to specify multiple builders. ex: "
1763 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1764 "the try server waterfall for the builders name and the tests "
1765 "available. Can also be used to specify gtest_filter, e.g. "
1766 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1767 group.add_option(
1768 "-r", "--revision",
1769 help="Revision to use for the try job; default: the "
1770 "revision will be determined by the try server; see "
1771 "its waterfall for more info")
1772 group.add_option(
1773 "-c", "--clobber", action="store_true", default=False,
1774 help="Force a clobber before building; e.g. don't do an "
1775 "incremental build")
1776 group.add_option(
1777 "--project",
1778 help="Override which project to use. Projects are defined "
1779 "server-side to define what default bot set to use")
1780 group.add_option(
1781 "-t", "--testfilter", action="append", default=[],
1782 help=("Apply a testfilter to all the selected builders. Unless the "
1783 "builders configurations are similar, use multiple "
1784 "--bot <builder>:<test> arguments."))
1785 group.add_option(
1786 "-n", "--name", help="Try job name; default to current branch name")
1787 parser.add_option_group(group)
1788 options, args = parser.parse_args(args)
1789
1790 if args:
1791 parser.error('Unknown arguments: %s' % args)
1792
1793 cl = Changelist()
1794 if not cl.GetIssue():
1795 parser.error('Need to upload first')
1796
1797 if not options.name:
1798 options.name = cl.GetBranch()
1799
1800 # Process --bot and --testfilter.
1801 if not options.bot:
1802 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001803 change = cl.GetChange(
1804 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1805 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001806 options.bot = presubmit_support.DoGetTrySlaves(
1807 change,
1808 change.LocalPaths(),
1809 settings.GetRoot(),
1810 None,
1811 None,
1812 options.verbose,
1813 sys.stdout)
1814 if not options.bot:
1815 parser.error('No default try builder to try, use --bot')
1816
1817 builders_and_tests = {}
1818 for bot in options.bot:
1819 if ':' in bot:
1820 builder, tests = bot.split(':', 1)
1821 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1822 elif ',' in bot:
1823 parser.error('Specify one bot per --bot flag')
1824 else:
1825 builders_and_tests.setdefault(bot, []).append('defaulttests')
1826
1827 if options.testfilter:
1828 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1829 builders_and_tests = dict(
1830 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1831 if t != ['compile'])
1832
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001833 if any('triggered' in b for b in builders_and_tests):
1834 print >> sys.stderr, (
1835 'ERROR You are trying to send a job to a triggered bot. This type of'
1836 ' bot requires an\ninitial job from a parent (usually a builder). '
1837 'Instead send your job to the parent.\n'
1838 'Bot list: %s' % builders_and_tests)
1839 return 1
1840
maruel@chromium.org15192402012-09-06 12:38:29 +00001841 patchset = cl.GetPatchset()
1842 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001843 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001844
1845 cl.RpcServer().trigger_try_jobs(
1846 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1847 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001848 print('Tried jobs on:')
1849 length = max(len(builder) for builder in builders_and_tests)
1850 for builder in sorted(builders_and_tests):
1851 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001852 return 0
1853
1854
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001855@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001856def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001857 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001858 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001859 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001860 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001861 return 0
1862
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001864 if args:
1865 # One arg means set upstream branch.
1866 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1867 cl = Changelist()
1868 print "Upstream branch set to " + cl.GetUpstreamBranch()
1869 else:
1870 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 return 0
1872
1873
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001874def CMDset_commit(parser, args):
1875 """set the commit bit"""
1876 _, args = parser.parse_args(args)
1877 if args:
1878 parser.error('Unrecognized args: %s' % ' '.join(args))
1879 cl = Changelist()
1880 cl.SetFlag('commit', '1')
1881 return 0
1882
1883
groby@chromium.org411034a2013-02-26 15:12:01 +00001884def CMDset_close(parser, args):
1885 """close the issue"""
1886 _, args = parser.parse_args(args)
1887 if args:
1888 parser.error('Unrecognized args: %s' % ' '.join(args))
1889 cl = Changelist()
1890 # Ensure there actually is an issue to close.
1891 cl.GetDescription()
1892 cl.CloseIssue()
1893 return 0
1894
1895
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896def Command(name):
1897 return getattr(sys.modules[__name__], 'CMD' + name, None)
1898
1899
1900def CMDhelp(parser, args):
1901 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001902 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 if len(args) == 1:
1904 return main(args + ['--help'])
1905 parser.print_help()
1906 return 0
1907
1908
1909def GenUsage(parser, command):
1910 """Modify an OptParse object with the function's documentation."""
1911 obj = Command(command)
1912 more = getattr(obj, 'usage_more', '')
1913 if command == 'help':
1914 command = '<command>'
1915 else:
1916 # OptParser.description prefer nicely non-formatted strings.
1917 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1918 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1919
1920
1921def main(argv):
1922 """Doesn't parse the arguments here, just find the right subcommand to
1923 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001924 if sys.hexversion < 0x02060000:
1925 print >> sys.stderr, (
1926 '\nYour python version %s is unsupported, please upgrade.\n' %
1927 sys.version.split(' ', 1)[0])
1928 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001929 # Reload settings.
1930 global settings
1931 settings = Settings()
1932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 # Do it late so all commands are listed.
1934 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1935 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1936 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1937
1938 # Create the option parse and add --verbose support.
1939 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001940 parser.add_option(
1941 '-v', '--verbose', action='count', default=0,
1942 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 old_parser_args = parser.parse_args
1944 def Parse(args):
1945 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001946 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001948 elif options.verbose:
1949 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950 else:
1951 logging.basicConfig(level=logging.WARNING)
1952 return options, args
1953 parser.parse_args = Parse
1954
1955 if argv:
1956 command = Command(argv[0])
1957 if command:
1958 # "fix" the usage and the description now that we know the subcommand.
1959 GenUsage(parser, argv[0])
1960 try:
1961 return command(parser, argv[1:])
1962 except urllib2.HTTPError, e:
1963 if e.code != 500:
1964 raise
1965 DieWithError(
1966 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1967 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1968
1969 # Not a known command. Default to help.
1970 GenUsage(parser, 'help')
1971 return CMDhelp(parser, argv)
1972
1973
1974if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001975 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 sys.exit(main(sys.argv[1:]))