blob: 961a4abb167e48aee8b939f43417394029c82816 [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(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000281 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282 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'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000841 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000842 '# 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:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001364 git_set_branch_value('last-upload-hash',
1365 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001366
1367 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001368
1369
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001370def IsSubmoduleMergeCommit(ref):
1371 # When submodules are added to the repo, we expect there to be a single
1372 # non-git-svn merge commit at remote HEAD with a signature comment.
1373 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001374 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001375 return RunGit(cmd) != ''
1376
1377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378def SendUpstream(parser, args, cmd):
1379 """Common code for CmdPush and CmdDCommit
1380
1381 Squashed commit into a single.
1382 Updates changelog with metadata (e.g. pointer to review).
1383 Pushes/dcommits the code upstream.
1384 Updates review and closes.
1385 """
1386 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1387 help='bypass upload presubmit hook')
1388 parser.add_option('-m', dest='message',
1389 help="override review description")
1390 parser.add_option('-f', action='store_true', dest='force',
1391 help="force yes to questions (don't prompt)")
1392 parser.add_option('-c', dest='contributor',
1393 help="external contributor for patch (appended to " +
1394 "description and used as author for git). Should be " +
1395 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001396 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 (options, args) = parser.parse_args(args)
1398 cl = Changelist()
1399
1400 if not args or cmd == 'push':
1401 # Default to merging against our best guess of the upstream branch.
1402 args = [cl.GetUpstreamBranch()]
1403
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001404 if options.contributor:
1405 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1406 print "Please provide contibutor as 'First Last <email@example.com>'"
1407 return 1
1408
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001410 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
ukai@chromium.org259e4682012-10-25 07:36:33 +00001412 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 return 1
1414
1415 # This rev-list syntax means "show all commits not in my branch that
1416 # are in base_branch".
1417 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1418 base_branch]).splitlines()
1419 if upstream_commits:
1420 print ('Base branch "%s" has %d commits '
1421 'not in this branch.' % (base_branch, len(upstream_commits)))
1422 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1423 return 1
1424
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001425 # This is the revision `svn dcommit` will commit on top of.
1426 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1427 '--pretty=format:%H'])
1428
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001430 # If the base_head is a submodule merge commit, the first parent of the
1431 # base_head should be a git-svn commit, which is what we're interested in.
1432 base_svn_head = base_branch
1433 if base_has_submodules:
1434 base_svn_head += '^1'
1435
1436 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 if extra_commits:
1438 print ('This branch has %d additional commits not upstreamed yet.'
1439 % len(extra_commits.splitlines()))
1440 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1441 'before attempting to %s.' % (base_branch, cmd))
1442 return 1
1443
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001444 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001445 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001446 author = None
1447 if options.contributor:
1448 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001449 hook_results = cl.RunHook(
1450 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001451 may_prompt=not options.force,
1452 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001453 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001454 if not hook_results.should_continue():
1455 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456
1457 if cmd == 'dcommit':
1458 # Check the tree status if the tree status URL is set.
1459 status = GetTreeStatus()
1460 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001461 print('The tree is closed. Please wait for it to reopen. Use '
1462 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 return 1
1464 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001465 print('Unable to determine tree status. Please verify manually and '
1466 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001467 else:
1468 breakpad.SendStack(
1469 'GitClHooksBypassedCommit',
1470 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001471 (cl.GetRietveldServer(), cl.GetIssue()),
1472 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001474 change_desc = ChangeDescription(options.message)
1475 if not change_desc.description and cl.GetIssue():
1476 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001478 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001479 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001480 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001481 else:
1482 print 'No description set.'
1483 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1484 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001486 # Keep a separate copy for the commit message, because the commit message
1487 # contains the link to the Rietveld issue, while the Rietveld message contains
1488 # the commit viewvc url.
1489 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001490 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001491 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001493 commit_desc.append_footer('Patch from %s.' % options.contributor)
1494
1495 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496
1497 branches = [base_branch, cl.GetBranchRef()]
1498 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001499 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001500 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001502 # We want to squash all this branch's commits into one commit with the proper
1503 # description. We do this by doing a "reset --soft" to the base branch (which
1504 # keeps the working copy the same), then dcommitting that. If origin/master
1505 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1506 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001508 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1509 # Delete the branches if they exist.
1510 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1511 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1512 result = RunGitWithCode(showref_cmd)
1513 if result[0] == 0:
1514 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515
1516 # We might be in a directory that's present in this branch but not in the
1517 # trunk. Move up to the top of the tree so that git commands that expect a
1518 # valid CWD won't fail after we check out the merge branch.
1519 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1520 if rel_base_path:
1521 os.chdir(rel_base_path)
1522
1523 # Stuff our change into the merge branch.
1524 # We wrap in a try...finally block so if anything goes wrong,
1525 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001526 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001528 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1529 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001531 RunGit(
1532 [
1533 'commit', '--author', options.contributor,
1534 '-m', commit_desc.description,
1535 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001538 if base_has_submodules:
1539 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1540 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1541 RunGit(['checkout', CHERRY_PICK_BRANCH])
1542 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543 if cmd == 'push':
1544 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001545 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546 retcode, output = RunGitWithCode(
1547 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1548 logging.debug(output)
1549 else:
1550 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001551 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001552 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001553 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554 finally:
1555 # And then swap back to the original branch and clean up.
1556 RunGit(['checkout', '-q', cl.GetBranch()])
1557 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001558 if base_has_submodules:
1559 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560
1561 if cl.GetIssue():
1562 if cmd == 'dcommit' and 'Committed r' in output:
1563 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1564 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001565 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1566 for l in output.splitlines(False))
1567 match = filter(None, match)
1568 if len(match) != 1:
1569 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1570 output)
1571 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572 else:
1573 return 1
1574 viewvc_url = settings.GetViewVCUrl()
1575 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001576 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001577 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001578 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579 print ('Closing issue '
1580 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001581 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001583 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001584 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001585 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001586 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1587 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001589
1590 if retcode == 0:
1591 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1592 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001593 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001594
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001595 return 0
1596
1597
1598@usage('[upstream branch to apply against]')
1599def CMDdcommit(parser, args):
1600 """commit the current changelist via git-svn"""
1601 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001602 message = """This doesn't appear to be an SVN repository.
1603If your project has a git mirror with an upstream SVN master, you probably need
1604to run 'git svn init', see your project's git mirror documentation.
1605If your project has a true writeable upstream repository, you probably want
1606to run 'git cl push' instead.
1607Choose wisely, if you get this wrong, your commit might appear to succeed but
1608will instead be silently ignored."""
1609 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001610 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611 return SendUpstream(parser, args, 'dcommit')
1612
1613
1614@usage('[upstream branch to apply against]')
1615def CMDpush(parser, args):
1616 """commit the current changelist via git"""
1617 if settings.GetIsGitSvn():
1618 print('This appears to be an SVN repository.')
1619 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001620 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621 return SendUpstream(parser, args, 'push')
1622
1623
1624@usage('<patch url or issue id>')
1625def CMDpatch(parser, args):
1626 """patch in a code review"""
1627 parser.add_option('-b', dest='newbranch',
1628 help='create a new branch off trunk for the patch')
1629 parser.add_option('-f', action='store_true', dest='force',
1630 help='with -b, clobber any existing branch')
1631 parser.add_option('--reject', action='store_true', dest='reject',
1632 help='allow failed patches and spew .rej files')
1633 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1634 help="don't commit after patch applies")
1635 (options, args) = parser.parse_args(args)
1636 if len(args) != 1:
1637 parser.print_help()
1638 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001639 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001641 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001642 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001643
maruel@chromium.org52424302012-08-29 15:14:30 +00001644 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001646 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001647 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001648 patchset = cl.GetMostRecentPatchset(issue)
1649 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001651 # Assume it's a URL to the patch. Default to https.
1652 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001653 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001654 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655 DieWithError('Must pass an issue ID or full URL for '
1656 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001657 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001658 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001659 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001660
1661 if options.newbranch:
1662 if options.force:
1663 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001664 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001665 RunGit(['checkout', '-b', options.newbranch,
1666 Changelist().GetUpstreamBranch()])
1667
1668 # Switch up to the top-level directory, if necessary, in preparation for
1669 # applying the patch.
1670 top = RunGit(['rev-parse', '--show-cdup']).strip()
1671 if top:
1672 os.chdir(top)
1673
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674 # Git patches have a/ at the beginning of source paths. We strip that out
1675 # with a sed script rather than the -p flag to patch so we can feed either
1676 # Git or svn-style patches into the same apply command.
1677 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001678 try:
1679 patch_data = subprocess2.check_output(
1680 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1681 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 DieWithError('Git patch mungling failed.')
1683 logging.info(patch_data)
1684 # We use "git apply" to apply the patch instead of "patch" so that we can
1685 # pick up file adds.
1686 # The --index flag means: also insert into the index (so we catch adds).
1687 cmd = ['git', 'apply', '--index', '-p0']
1688 if options.reject:
1689 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001690 try:
1691 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1692 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001693 DieWithError('Failed to apply the patch')
1694
1695 # If we had an issue, commit the current state and register the issue.
1696 if not options.nocommit:
1697 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1698 cl = Changelist()
1699 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001700 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001701 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001702 else:
1703 print "Patch applied to index."
1704 return 0
1705
1706
1707def CMDrebase(parser, args):
1708 """rebase current branch on top of svn repo"""
1709 # Provide a wrapper for git svn rebase to help avoid accidental
1710 # git svn dcommit.
1711 # It's the only command that doesn't use parser at all since we just defer
1712 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001713 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001714
1715
1716def GetTreeStatus():
1717 """Fetches the tree status and returns either 'open', 'closed',
1718 'unknown' or 'unset'."""
1719 url = settings.GetTreeStatusUrl(error_ok=True)
1720 if url:
1721 status = urllib2.urlopen(url).read().lower()
1722 if status.find('closed') != -1 or status == '0':
1723 return 'closed'
1724 elif status.find('open') != -1 or status == '1':
1725 return 'open'
1726 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001727 return 'unset'
1728
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001729
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730def GetTreeStatusReason():
1731 """Fetches the tree status from a json url and returns the message
1732 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001733 url = settings.GetTreeStatusUrl()
1734 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 connection = urllib2.urlopen(json_url)
1736 status = json.loads(connection.read())
1737 connection.close()
1738 return status['message']
1739
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001741def CMDtree(parser, args):
1742 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001743 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001744 status = GetTreeStatus()
1745 if 'unset' == status:
1746 print 'You must configure your tree status URL by running "git cl config".'
1747 return 2
1748
1749 print "The tree is %s" % status
1750 print
1751 print GetTreeStatusReason()
1752 if status != 'open':
1753 return 1
1754 return 0
1755
1756
maruel@chromium.org15192402012-09-06 12:38:29 +00001757def CMDtry(parser, args):
1758 """Triggers a try job through Rietveld."""
1759 group = optparse.OptionGroup(parser, "Try job options")
1760 group.add_option(
1761 "-b", "--bot", action="append",
1762 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1763 "times to specify multiple builders. ex: "
1764 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1765 "the try server waterfall for the builders name and the tests "
1766 "available. Can also be used to specify gtest_filter, e.g. "
1767 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1768 group.add_option(
1769 "-r", "--revision",
1770 help="Revision to use for the try job; default: the "
1771 "revision will be determined by the try server; see "
1772 "its waterfall for more info")
1773 group.add_option(
1774 "-c", "--clobber", action="store_true", default=False,
1775 help="Force a clobber before building; e.g. don't do an "
1776 "incremental build")
1777 group.add_option(
1778 "--project",
1779 help="Override which project to use. Projects are defined "
1780 "server-side to define what default bot set to use")
1781 group.add_option(
1782 "-t", "--testfilter", action="append", default=[],
1783 help=("Apply a testfilter to all the selected builders. Unless the "
1784 "builders configurations are similar, use multiple "
1785 "--bot <builder>:<test> arguments."))
1786 group.add_option(
1787 "-n", "--name", help="Try job name; default to current branch name")
1788 parser.add_option_group(group)
1789 options, args = parser.parse_args(args)
1790
1791 if args:
1792 parser.error('Unknown arguments: %s' % args)
1793
1794 cl = Changelist()
1795 if not cl.GetIssue():
1796 parser.error('Need to upload first')
1797
1798 if not options.name:
1799 options.name = cl.GetBranch()
1800
1801 # Process --bot and --testfilter.
1802 if not options.bot:
1803 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001804 change = cl.GetChange(
1805 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1806 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001807 options.bot = presubmit_support.DoGetTrySlaves(
1808 change,
1809 change.LocalPaths(),
1810 settings.GetRoot(),
1811 None,
1812 None,
1813 options.verbose,
1814 sys.stdout)
1815 if not options.bot:
1816 parser.error('No default try builder to try, use --bot')
1817
1818 builders_and_tests = {}
1819 for bot in options.bot:
1820 if ':' in bot:
1821 builder, tests = bot.split(':', 1)
1822 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1823 elif ',' in bot:
1824 parser.error('Specify one bot per --bot flag')
1825 else:
1826 builders_and_tests.setdefault(bot, []).append('defaulttests')
1827
1828 if options.testfilter:
1829 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1830 builders_and_tests = dict(
1831 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1832 if t != ['compile'])
1833
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001834 if any('triggered' in b for b in builders_and_tests):
1835 print >> sys.stderr, (
1836 'ERROR You are trying to send a job to a triggered bot. This type of'
1837 ' bot requires an\ninitial job from a parent (usually a builder). '
1838 'Instead send your job to the parent.\n'
1839 'Bot list: %s' % builders_and_tests)
1840 return 1
1841
maruel@chromium.org15192402012-09-06 12:38:29 +00001842 patchset = cl.GetPatchset()
1843 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001844 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001845
1846 cl.RpcServer().trigger_try_jobs(
1847 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1848 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001849 print('Tried jobs on:')
1850 length = max(len(builder) for builder in builders_and_tests)
1851 for builder in sorted(builders_and_tests):
1852 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001853 return 0
1854
1855
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001856@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001857def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001858 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001859 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001860 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001861 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001862 return 0
1863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001865 if args:
1866 # One arg means set upstream branch.
1867 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1868 cl = Changelist()
1869 print "Upstream branch set to " + cl.GetUpstreamBranch()
1870 else:
1871 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872 return 0
1873
1874
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001875def CMDset_commit(parser, args):
1876 """set the commit bit"""
1877 _, args = parser.parse_args(args)
1878 if args:
1879 parser.error('Unrecognized args: %s' % ' '.join(args))
1880 cl = Changelist()
1881 cl.SetFlag('commit', '1')
1882 return 0
1883
1884
groby@chromium.org411034a2013-02-26 15:12:01 +00001885def CMDset_close(parser, args):
1886 """close the issue"""
1887 _, args = parser.parse_args(args)
1888 if args:
1889 parser.error('Unrecognized args: %s' % ' '.join(args))
1890 cl = Changelist()
1891 # Ensure there actually is an issue to close.
1892 cl.GetDescription()
1893 cl.CloseIssue()
1894 return 0
1895
1896
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897def Command(name):
1898 return getattr(sys.modules[__name__], 'CMD' + name, None)
1899
1900
1901def CMDhelp(parser, args):
1902 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001903 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001904 if len(args) == 1:
1905 return main(args + ['--help'])
1906 parser.print_help()
1907 return 0
1908
1909
1910def GenUsage(parser, command):
1911 """Modify an OptParse object with the function's documentation."""
1912 obj = Command(command)
1913 more = getattr(obj, 'usage_more', '')
1914 if command == 'help':
1915 command = '<command>'
1916 else:
1917 # OptParser.description prefer nicely non-formatted strings.
1918 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1919 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1920
1921
1922def main(argv):
1923 """Doesn't parse the arguments here, just find the right subcommand to
1924 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001925 if sys.hexversion < 0x02060000:
1926 print >> sys.stderr, (
1927 '\nYour python version %s is unsupported, please upgrade.\n' %
1928 sys.version.split(' ', 1)[0])
1929 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001930 # Reload settings.
1931 global settings
1932 settings = Settings()
1933
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934 # Do it late so all commands are listed.
1935 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1936 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1937 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1938
1939 # Create the option parse and add --verbose support.
1940 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001941 parser.add_option(
1942 '-v', '--verbose', action='count', default=0,
1943 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001944 old_parser_args = parser.parse_args
1945 def Parse(args):
1946 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001947 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001949 elif options.verbose:
1950 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951 else:
1952 logging.basicConfig(level=logging.WARNING)
1953 return options, args
1954 parser.parse_args = Parse
1955
1956 if argv:
1957 command = Command(argv[0])
1958 if command:
1959 # "fix" the usage and the description now that we know the subcommand.
1960 GenUsage(parser, argv[0])
1961 try:
1962 return command(parser, argv[1:])
1963 except urllib2.HTTPError, e:
1964 if e.code != 500:
1965 raise
1966 DieWithError(
1967 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1968 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1969
1970 # Not a known command. Default to help.
1971 GenUsage(parser, 'help')
1972 return CMDhelp(parser, argv)
1973
1974
1975if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001976 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977 sys.exit(main(sys.argv[1:]))