blob: 7b708fc5b35b4cad7892343678d6ebaacb66ed40 [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
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000650 def GetApprovingReviewers(self, issue):
651 return get_approving_reviewers(
652 self.RpcServer().get_issue_properties(int(issue), True))
653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000654 def SetIssue(self, issue):
655 """Set this branch's issue. If issue=0, clears the issue."""
656 if issue:
657 RunGit(['config', self._IssueSetting(), str(issue)])
658 if self.rietveld_server:
659 RunGit(['config', self._RietveldServer(), self.rietveld_server])
660 else:
661 RunGit(['config', '--unset', self._IssueSetting()])
662 self.SetPatchset(0)
663 self.has_issue = False
664
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000665 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000666 if not self.GitSanityChecks(upstream_branch):
667 DieWithError('\nGit sanity check failure')
668
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000669 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
670 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000671
672 # We use the sha1 of HEAD as a name of this change.
673 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000674 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000675 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000676 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000677 except subprocess2.CalledProcessError:
678 DieWithError(
679 ('\nFailed to diff against upstream branch %s!\n\n'
680 'This branch probably doesn\'t exist anymore. To reset the\n'
681 'tracking branch, please run\n'
682 ' git branch --set-upstream %s trunk\n'
683 'replacing trunk with origin/master or the relevant branch') %
684 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000685
maruel@chromium.org52424302012-08-29 15:14:30 +0000686 issue = self.GetIssue()
687 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000688 if issue:
689 description = self.GetDescription()
690 else:
691 # If the change was never uploaded, use the log messages of all commits
692 # up to the branch point, as git cl upload will prefill the description
693 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000694 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
695 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000696
697 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000698 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000699 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000700 name,
701 description,
702 absroot,
703 files,
704 issue,
705 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000706 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000707
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000708 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000709 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000710
711 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000712 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000713 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000714 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000715 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000716 except presubmit_support.PresubmitFailure, e:
717 DieWithError(
718 ('%s\nMaybe your depot_tools is out of date?\n'
719 'If all fails, contact maruel@') % e)
720
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000721 def UpdateDescription(self, description):
722 self.description = description
723 return self.RpcServer().update_description(
724 self.GetIssue(), self.description)
725
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000727 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000728 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000730 def SetFlag(self, flag, value):
731 """Patchset must match."""
732 if not self.GetPatchset():
733 DieWithError('The patchset needs to match. Send another patchset.')
734 try:
735 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000736 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000737 except urllib2.HTTPError, e:
738 if e.code == 404:
739 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
740 if e.code == 403:
741 DieWithError(
742 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
743 'match?') % (self.GetIssue(), self.GetPatchset()))
744 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000746 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 """Returns an upload.RpcServer() to access this review's rietveld instance.
748 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000749 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000750 self._rpc_server = rietveld.CachingRietveld(
751 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000752 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753
754 def _IssueSetting(self):
755 """Return the git setting that stores this change's issue."""
756 return 'branch.%s.rietveldissue' % self.GetBranch()
757
758 def _PatchsetSetting(self):
759 """Return the git setting that stores this change's most recent patchset."""
760 return 'branch.%s.rietveldpatchset' % self.GetBranch()
761
762 def _RietveldServer(self):
763 """Returns the git setting that stores this change's rietveld server."""
764 return 'branch.%s.rietveldserver' % self.GetBranch()
765
766
767def GetCodereviewSettingsInteractively():
768 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000769 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 server = settings.GetDefaultServerUrl(error_ok=True)
771 prompt = 'Rietveld server (host[:port])'
772 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000773 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774 if not server and not newserver:
775 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000776 if newserver:
777 newserver = gclient_utils.UpgradeToHttps(newserver)
778 if newserver != server:
779 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000781 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 prompt = caption
783 if initial:
784 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000785 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 if new_val == 'x':
787 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000788 elif new_val:
789 if is_url:
790 new_val = gclient_utils.UpgradeToHttps(new_val)
791 if new_val != initial:
792 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000794 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000796 'tree-status-url', False)
797 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798
799 # TODO: configure a default branch to diff against, rather than this
800 # svn-based hackery.
801
802
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000803class ChangeDescription(object):
804 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000805 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000806
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000807 def __init__(self, description):
808 self._description = (description or '').strip()
809
810 @property
811 def description(self):
812 return self._description
813
814 def update_reviewers(self, reviewers):
815 """Rewrites the R=/TBR= line(s) as a single line."""
816 assert isinstance(reviewers, list), reviewers
817 if not reviewers:
818 return
819 regexp = re.compile(self.R_LINE, re.MULTILINE)
820 matches = list(regexp.finditer(self._description))
821 is_tbr = any(m.group(1) == 'TBR' for m in matches)
822 if len(matches) > 1:
823 # Erase all except the first one.
824 for i in xrange(len(matches) - 1, 0, -1):
825 self._description = (
826 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000827 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000828
829 if is_tbr:
830 new_r_line = 'TBR=' + ', '.join(reviewers)
831 else:
832 new_r_line = 'R=' + ', '.join(reviewers)
833
834 if matches:
835 self._description = (
836 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000837 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000838 else:
839 self.append_footer(new_r_line)
840
841 def prompt(self):
842 """Asks the user to update the description."""
843 self._description = (
844 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000845 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000846 '# The first line will also be used as the subject of the review.\n'
847 ) + self._description
848
849 if '\nBUG=' not in self._description:
850 self.append_footer('BUG=')
851 content = gclient_utils.RunEditor(self._description, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000852 if not content:
853 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000854
855 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000856 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000857 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000858 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000859 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000860
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000861 def append_footer(self, line):
862 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
863 if self._description:
864 if '\n' not in self._description:
865 self._description += '\n'
866 else:
867 last_line = self._description.rsplit('\n', 1)[1]
868 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
869 not presubmit_support.Change.TAG_LINE_RE.match(line)):
870 self._description += '\n'
871 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000872
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000873 def get_reviewers(self):
874 """Retrieves the list of reviewers."""
875 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000876 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000877 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000878
879
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000880def get_approving_reviewers(props):
881 """Retrieves the reviewers that approved a CL from the issue properties with
882 messages.
883
884 Note that the list may contain reviewers that are not committer, thus are not
885 considered by the CQ.
886 """
887 return sorted(
888 set(
889 message['sender']
890 for message in props['messages']
891 if message['approval'] and message['sender'] in props['reviewers']
892 )
893 )
894
895
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896def FindCodereviewSettingsFile(filename='codereview.settings'):
897 """Finds the given file starting in the cwd and going up.
898
899 Only looks up to the top of the repository unless an
900 'inherit-review-settings-ok' file exists in the root of the repository.
901 """
902 inherit_ok_file = 'inherit-review-settings-ok'
903 cwd = os.getcwd()
904 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
905 if os.path.isfile(os.path.join(root, inherit_ok_file)):
906 root = '/'
907 while True:
908 if filename in os.listdir(cwd):
909 if os.path.isfile(os.path.join(cwd, filename)):
910 return open(os.path.join(cwd, filename))
911 if cwd == root:
912 break
913 cwd = os.path.dirname(cwd)
914
915
916def LoadCodereviewSettingsFromFile(fileobj):
917 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000918 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000920 def SetProperty(name, setting, unset_error_ok=False):
921 fullname = 'rietveld.' + name
922 if setting in keyvals:
923 RunGit(['config', fullname, keyvals[setting]])
924 else:
925 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
926
927 SetProperty('server', 'CODE_REVIEW_SERVER')
928 # Only server setting is required. Other settings can be absent.
929 # In that case, we ignore errors raised during option deletion attempt.
930 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
931 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
932 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
933
ukai@chromium.orge8077812012-02-03 03:41:46 +0000934 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
935 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
936 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
939 #should be of the form
940 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
941 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
942 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
943 keyvals['ORIGIN_URL_CONFIG']])
944
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000946def urlretrieve(source, destination):
947 """urllib is broken for SSL connections via a proxy therefore we
948 can't use urllib.urlretrieve()."""
949 with open(destination, 'w') as f:
950 f.write(urllib2.urlopen(source).read())
951
952
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000953def DownloadHooks(force):
954 """downloads hooks
955
956 Args:
957 force: True to update hooks. False to install hooks if not present.
958 """
959 if not settings.GetIsGerrit():
960 return
961 server_url = settings.GetDefaultServerUrl()
962 src = '%s/tools/hooks/commit-msg' % server_url
963 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
964 if not os.access(dst, os.X_OK):
965 if os.path.exists(dst):
966 if not force:
967 return
968 os.remove(dst)
969 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000970 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000971 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
972 except Exception:
973 if os.path.exists(dst):
974 os.remove(dst)
975 DieWithError('\nFailed to download hooks from %s' % src)
976
977
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978@usage('[repo root containing codereview.settings]')
979def CMDconfig(parser, args):
980 """edit configuration for this tree"""
981
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000982 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983 if len(args) == 0:
984 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000985 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986 return 0
987
988 url = args[0]
989 if not url.endswith('codereview.settings'):
990 url = os.path.join(url, 'codereview.settings')
991
992 # Load code review settings and download hooks (if available).
993 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000994 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995 return 0
996
997
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000998def CMDbaseurl(parser, args):
999 """get or set base-url for this branch"""
1000 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1001 branch = ShortBranchName(branchref)
1002 _, args = parser.parse_args(args)
1003 if not args:
1004 print("Current base-url:")
1005 return RunGit(['config', 'branch.%s.base-url' % branch],
1006 error_ok=False).strip()
1007 else:
1008 print("Setting base-url to %s" % args[0])
1009 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1010 error_ok=False).strip()
1011
1012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013def CMDstatus(parser, args):
1014 """show status of changelists"""
1015 parser.add_option('--field',
1016 help='print only specific field (desc|id|patch|url)')
1017 (options, args) = parser.parse_args(args)
1018
1019 # TODO: maybe make show_branches a flag if necessary.
1020 show_branches = not options.field
1021
1022 if show_branches:
1023 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1024 if branches:
1025 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001026 changes = (Changelist(branchref=b) for b in branches.splitlines())
1027 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1028 alignment = max(5, max(len(b) for b in branches))
1029 for branch in sorted(branches):
1030 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
1032 cl = Changelist()
1033 if options.field:
1034 if options.field.startswith('desc'):
1035 print cl.GetDescription()
1036 elif options.field == 'id':
1037 issueid = cl.GetIssue()
1038 if issueid:
1039 print issueid
1040 elif options.field == 'patch':
1041 patchset = cl.GetPatchset()
1042 if patchset:
1043 print patchset
1044 elif options.field == 'url':
1045 url = cl.GetIssueURL()
1046 if url:
1047 print url
1048 else:
1049 print
1050 print 'Current branch:',
1051 if not cl.GetIssue():
1052 print 'no issue assigned.'
1053 return 0
1054 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001055 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 print 'Issue description:'
1057 print cl.GetDescription(pretty=True)
1058 return 0
1059
1060
1061@usage('[issue_number]')
1062def CMDissue(parser, args):
1063 """Set or display the current code review issue number.
1064
1065 Pass issue number 0 to clear the current issue.
1066"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001067 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068
1069 cl = Changelist()
1070 if len(args) > 0:
1071 try:
1072 issue = int(args[0])
1073 except ValueError:
1074 DieWithError('Pass a number to set the issue or none to list it.\n'
1075 'Maybe you want to run git cl status?')
1076 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001077 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 return 0
1079
1080
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001081def CMDcomments(parser, args):
1082 """show review comments of the current changelist"""
1083 (_, args) = parser.parse_args(args)
1084 if args:
1085 parser.error('Unsupported argument: %s' % args)
1086
1087 cl = Changelist()
1088 if cl.GetIssue():
1089 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1090 for message in sorted(data['messages'], key=lambda x: x['date']):
1091 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
1092 if message['text'].strip():
1093 print '\n'.join(' ' + l for l in message['text'].splitlines())
1094 return 0
1095
1096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097def CreateDescriptionFromLog(args):
1098 """Pulls out the commit log to use as a base for the CL description."""
1099 log_args = []
1100 if len(args) == 1 and not args[0].endswith('.'):
1101 log_args = [args[0] + '..']
1102 elif len(args) == 1 and args[0].endswith('...'):
1103 log_args = [args[0][:-1]]
1104 elif len(args) == 2:
1105 log_args = [args[0] + '..' + args[1]]
1106 else:
1107 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001108 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109
1110
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111def CMDpresubmit(parser, args):
1112 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001113 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001115 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001116 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 (options, args) = parser.parse_args(args)
1118
ukai@chromium.org259e4682012-10-25 07:36:33 +00001119 if not options.force and is_dirty_git_tree('presubmit'):
1120 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121 return 1
1122
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001123 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124 if args:
1125 base_branch = args[0]
1126 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001127 # Default to diffing against the common ancestor of the upstream branch.
1128 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001130 cl.RunHook(
1131 committing=not options.upload,
1132 may_prompt=False,
1133 verbose=options.verbose,
1134 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001135 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136
1137
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001138def AddChangeIdToCommitMessage(options, args):
1139 """Re-commits using the current message, assumes the commit hook is in
1140 place.
1141 """
1142 log_desc = options.message or CreateDescriptionFromLog(args)
1143 git_command = ['commit', '--amend', '-m', log_desc]
1144 RunGit(git_command)
1145 new_log_desc = CreateDescriptionFromLog(args)
1146 if CHANGE_ID in new_log_desc:
1147 print 'git-cl: Added Change-Id to commit message.'
1148 else:
1149 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1150
1151
ukai@chromium.orge8077812012-02-03 03:41:46 +00001152def GerritUpload(options, args, cl):
1153 """upload the current branch to gerrit."""
1154 # We assume the remote called "origin" is the one we want.
1155 # It is probably not worthwhile to support different workflows.
1156 remote = 'origin'
1157 branch = 'master'
1158 if options.target_branch:
1159 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001161 change_desc = ChangeDescription(
1162 options.message or CreateDescriptionFromLog(args))
1163 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001164 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001166 if CHANGE_ID not in change_desc.description:
1167 AddChangeIdToCommitMessage(options, args)
1168 if options.reviewers:
1169 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170
ukai@chromium.orge8077812012-02-03 03:41:46 +00001171 receive_options = []
1172 cc = cl.GetCCList().split(',')
1173 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001174 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001175 cc = filter(None, cc)
1176 if cc:
1177 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001178 if change_desc.get_reviewers():
1179 receive_options.extend(
1180 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181
ukai@chromium.orge8077812012-02-03 03:41:46 +00001182 git_command = ['push']
1183 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001184 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001185 ' '.join(receive_options))
1186 git_command += [remote, 'HEAD:refs/for/' + branch]
1187 RunGit(git_command)
1188 # TODO(ukai): parse Change-Id: and set issue number?
1189 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001190
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
ukai@chromium.orge8077812012-02-03 03:41:46 +00001192def RietveldUpload(options, args, cl):
1193 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1195 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 if options.emulate_svn_auto_props:
1197 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
1199 change_desc = None
1200
1201 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001202 if options.title:
1203 upload_args.extend(['--title', options.title])
1204 elif options.message:
1205 # TODO(rogerta): for now, the -m option will also set the --title option
1206 # for upload.py. Soon this will be changed to set the --message option.
1207 # Will wait until people are used to typing -t instead of -m.
1208 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001209 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 print ("This branch is associated with issue %s. "
1211 "Adding patch to that issue." % cl.GetIssue())
1212 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001213 if options.title:
1214 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001215 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001216 change_desc = ChangeDescription(message)
1217 if options.reviewers:
1218 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001219 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001220 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001221
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001222 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 print "Description is empty; aborting."
1224 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001225
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001226 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001227 if change_desc.get_reviewers():
1228 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001229 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001230 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001231 DieWithError("Must specify reviewers to send email.")
1232 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001233 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001234 if cc:
1235 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001237 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001238 if not options.find_copies:
1239 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 # Include the upstream repo's URL in the change -- this is useful for
1242 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001243 remote_url = cl.GetGitBaseUrlFromConfig()
1244 if not remote_url:
1245 if settings.GetIsGitSvn():
1246 # URL is dependent on the current directory.
1247 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1248 if data:
1249 keys = dict(line.split(': ', 1) for line in data.splitlines()
1250 if ': ' in line)
1251 remote_url = keys.get('URL', None)
1252 else:
1253 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1254 remote_url = (cl.GetRemoteUrl() + '@'
1255 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 if remote_url:
1257 upload_args.extend(['--base_url', remote_url])
1258
1259 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001260 upload_args = ['upload'] + upload_args + args
1261 logging.info('upload.RealMain(%s)', upload_args)
1262 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001263 except KeyboardInterrupt:
1264 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 except:
1266 # If we got an exception after the user typed a description for their
1267 # change, back up the description before re-raising.
1268 if change_desc:
1269 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1270 print '\nGot exception while uploading -- saving description to %s\n' \
1271 % backup_path
1272 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001273 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 backup_file.close()
1275 raise
1276
1277 if not cl.GetIssue():
1278 cl.SetIssue(issue)
1279 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001280
1281 if options.use_commit_queue:
1282 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001283 return 0
1284
1285
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001286def cleanup_list(l):
1287 """Fixes a list so that comma separated items are put as individual items.
1288
1289 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1290 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1291 """
1292 items = sum((i.split(',') for i in l), [])
1293 stripped_items = (i.strip() for i in items)
1294 return sorted(filter(None, stripped_items))
1295
1296
ukai@chromium.orge8077812012-02-03 03:41:46 +00001297@usage('[args to "git diff"]')
1298def CMDupload(parser, args):
1299 """upload the current changelist to codereview"""
1300 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1301 help='bypass upload presubmit hook')
1302 parser.add_option('-f', action='store_true', dest='force',
1303 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001304 parser.add_option('-m', dest='message', help='message for patchset')
1305 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001306 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001307 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001308 help='reviewer email addresses')
1309 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001310 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001311 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001312 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001313 help='send email to reviewer immediately')
1314 parser.add_option("--emulate_svn_auto_props", action="store_true",
1315 dest="emulate_svn_auto_props",
1316 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001317 parser.add_option('-c', '--use-commit-queue', action='store_true',
1318 help='tell the commit queue to commit this patchset')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001319 parser.add_option('--target_branch',
1320 help='When uploading to gerrit, remote branch to '
1321 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001322 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001323 (options, args) = parser.parse_args(args)
1324
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001325 if options.target_branch and not settings.GetIsGerrit():
1326 parser.error('Use --target_branch for non gerrit repository.')
1327
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001328 # Print warning if the user used the -m/--message argument. This will soon
1329 # change to -t/--title.
1330 if options.message:
1331 print >> sys.stderr, (
1332 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1333 'In the near future, -m or --message will send a message instead.\n'
1334 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001335
ukai@chromium.org259e4682012-10-25 07:36:33 +00001336 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001337 return 1
1338
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001339 options.reviewers = cleanup_list(options.reviewers)
1340 options.cc = cleanup_list(options.cc)
1341
ukai@chromium.orge8077812012-02-03 03:41:46 +00001342 cl = Changelist()
1343 if args:
1344 # TODO(ukai): is it ok for gerrit case?
1345 base_branch = args[0]
1346 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001347 # Default to diffing against common ancestor of upstream branch
1348 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001349 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001350
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001351 # Apply watchlists on upload.
1352 change = cl.GetChange(base_branch, None)
1353 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1354 files = [f.LocalPath() for f in change.AffectedFiles()]
1355 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
1356
ukai@chromium.orge8077812012-02-03 03:41:46 +00001357 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001358 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001359 may_prompt=not options.force,
1360 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001361 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001362 if not hook_results.should_continue():
1363 return 1
1364 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001365 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001366
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001367 if cl.GetIssue():
1368 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1369 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001370 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001371 print ('The last upload made from this repository was patchset #%d but '
1372 'the most recent patchset on the server is #%d.'
1373 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001374 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1375 'from another machine or branch the patch you\'re uploading now '
1376 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001377 ask_for_data('About to upload; enter to confirm.')
1378
iannucci@chromium.org79540052012-10-19 23:15:26 +00001379 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001380 if settings.GetIsGerrit():
1381 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001382 ret = RietveldUpload(options, args, cl)
1383 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001384 git_set_branch_value('last-upload-hash',
1385 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001386
1387 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001388
1389
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001390def IsSubmoduleMergeCommit(ref):
1391 # When submodules are added to the repo, we expect there to be a single
1392 # non-git-svn merge commit at remote HEAD with a signature comment.
1393 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001394 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001395 return RunGit(cmd) != ''
1396
1397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398def SendUpstream(parser, args, cmd):
1399 """Common code for CmdPush and CmdDCommit
1400
1401 Squashed commit into a single.
1402 Updates changelog with metadata (e.g. pointer to review).
1403 Pushes/dcommits the code upstream.
1404 Updates review and closes.
1405 """
1406 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1407 help='bypass upload presubmit hook')
1408 parser.add_option('-m', dest='message',
1409 help="override review description")
1410 parser.add_option('-f', action='store_true', dest='force',
1411 help="force yes to questions (don't prompt)")
1412 parser.add_option('-c', dest='contributor',
1413 help="external contributor for patch (appended to " +
1414 "description and used as author for git). Should be " +
1415 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001416 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 (options, args) = parser.parse_args(args)
1418 cl = Changelist()
1419
1420 if not args or cmd == 'push':
1421 # Default to merging against our best guess of the upstream branch.
1422 args = [cl.GetUpstreamBranch()]
1423
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001424 if options.contributor:
1425 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1426 print "Please provide contibutor as 'First Last <email@example.com>'"
1427 return 1
1428
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001430 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431
ukai@chromium.org259e4682012-10-25 07:36:33 +00001432 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 return 1
1434
1435 # This rev-list syntax means "show all commits not in my branch that
1436 # are in base_branch".
1437 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1438 base_branch]).splitlines()
1439 if upstream_commits:
1440 print ('Base branch "%s" has %d commits '
1441 'not in this branch.' % (base_branch, len(upstream_commits)))
1442 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1443 return 1
1444
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001445 # This is the revision `svn dcommit` will commit on top of.
1446 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1447 '--pretty=format:%H'])
1448
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001450 # If the base_head is a submodule merge commit, the first parent of the
1451 # base_head should be a git-svn commit, which is what we're interested in.
1452 base_svn_head = base_branch
1453 if base_has_submodules:
1454 base_svn_head += '^1'
1455
1456 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 if extra_commits:
1458 print ('This branch has %d additional commits not upstreamed yet.'
1459 % len(extra_commits.splitlines()))
1460 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1461 'before attempting to %s.' % (base_branch, cmd))
1462 return 1
1463
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001464 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001465 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001466 author = None
1467 if options.contributor:
1468 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001469 hook_results = cl.RunHook(
1470 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001471 may_prompt=not options.force,
1472 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001473 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001474 if not hook_results.should_continue():
1475 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476
1477 if cmd == 'dcommit':
1478 # Check the tree status if the tree status URL is set.
1479 status = GetTreeStatus()
1480 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001481 print('The tree is closed. Please wait for it to reopen. Use '
1482 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483 return 1
1484 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001485 print('Unable to determine tree status. Please verify manually and '
1486 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001487 else:
1488 breakpad.SendStack(
1489 'GitClHooksBypassedCommit',
1490 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001491 (cl.GetRietveldServer(), cl.GetIssue()),
1492 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001494 change_desc = ChangeDescription(options.message)
1495 if not change_desc.description and cl.GetIssue():
1496 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001498 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001499 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001500 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001501 else:
1502 print 'No description set.'
1503 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1504 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001506 # Keep a separate copy for the commit message, because the commit message
1507 # contains the link to the Rietveld issue, while the Rietveld message contains
1508 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001509 # Keep a separate copy for the commit message.
1510 if cl.GetIssue():
1511 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1512
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001513 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001514 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001515 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001517 commit_desc.append_footer('Patch from %s.' % options.contributor)
1518
1519 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520
1521 branches = [base_branch, cl.GetBranchRef()]
1522 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001523 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001524 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001525
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001526 # We want to squash all this branch's commits into one commit with the proper
1527 # description. We do this by doing a "reset --soft" to the base branch (which
1528 # keeps the working copy the same), then dcommitting that. If origin/master
1529 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1530 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001532 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1533 # Delete the branches if they exist.
1534 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1535 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1536 result = RunGitWithCode(showref_cmd)
1537 if result[0] == 0:
1538 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001539
1540 # We might be in a directory that's present in this branch but not in the
1541 # trunk. Move up to the top of the tree so that git commands that expect a
1542 # valid CWD won't fail after we check out the merge branch.
1543 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1544 if rel_base_path:
1545 os.chdir(rel_base_path)
1546
1547 # Stuff our change into the merge branch.
1548 # We wrap in a try...finally block so if anything goes wrong,
1549 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001550 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001552 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1553 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001555 RunGit(
1556 [
1557 'commit', '--author', options.contributor,
1558 '-m', commit_desc.description,
1559 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001561 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001562 if base_has_submodules:
1563 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1564 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1565 RunGit(['checkout', CHERRY_PICK_BRANCH])
1566 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567 if cmd == 'push':
1568 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001569 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001570 retcode, output = RunGitWithCode(
1571 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1572 logging.debug(output)
1573 else:
1574 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001575 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001576 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001577 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578 finally:
1579 # And then swap back to the original branch and clean up.
1580 RunGit(['checkout', '-q', cl.GetBranch()])
1581 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001582 if base_has_submodules:
1583 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584
1585 if cl.GetIssue():
1586 if cmd == 'dcommit' and 'Committed r' in output:
1587 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1588 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001589 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1590 for l in output.splitlines(False))
1591 match = filter(None, match)
1592 if len(match) != 1:
1593 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1594 output)
1595 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596 else:
1597 return 1
1598 viewvc_url = settings.GetViewVCUrl()
1599 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001600 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001601 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001602 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001603 print ('Closing issue '
1604 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001605 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001606 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001607 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001608 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001609 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001610 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1611 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001612 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001613
1614 if retcode == 0:
1615 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1616 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001617 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619 return 0
1620
1621
1622@usage('[upstream branch to apply against]')
1623def CMDdcommit(parser, args):
1624 """commit the current changelist via git-svn"""
1625 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001626 message = """This doesn't appear to be an SVN repository.
1627If your project has a git mirror with an upstream SVN master, you probably need
1628to run 'git svn init', see your project's git mirror documentation.
1629If your project has a true writeable upstream repository, you probably want
1630to run 'git cl push' instead.
1631Choose wisely, if you get this wrong, your commit might appear to succeed but
1632will instead be silently ignored."""
1633 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001634 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001635 return SendUpstream(parser, args, 'dcommit')
1636
1637
1638@usage('[upstream branch to apply against]')
1639def CMDpush(parser, args):
1640 """commit the current changelist via git"""
1641 if settings.GetIsGitSvn():
1642 print('This appears to be an SVN repository.')
1643 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001644 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645 return SendUpstream(parser, args, 'push')
1646
1647
1648@usage('<patch url or issue id>')
1649def CMDpatch(parser, args):
1650 """patch in a code review"""
1651 parser.add_option('-b', dest='newbranch',
1652 help='create a new branch off trunk for the patch')
1653 parser.add_option('-f', action='store_true', dest='force',
1654 help='with -b, clobber any existing branch')
1655 parser.add_option('--reject', action='store_true', dest='reject',
1656 help='allow failed patches and spew .rej files')
1657 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1658 help="don't commit after patch applies")
1659 (options, args) = parser.parse_args(args)
1660 if len(args) != 1:
1661 parser.print_help()
1662 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001663 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001664
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001665 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001666 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001667
maruel@chromium.org52424302012-08-29 15:14:30 +00001668 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001669 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001670 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001671 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001672 patchset = cl.GetMostRecentPatchset(issue)
1673 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001675 # Assume it's a URL to the patch. Default to https.
1676 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001677 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001678 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001679 DieWithError('Must pass an issue ID or full URL for '
1680 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001681 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001682 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001683 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001684
1685 if options.newbranch:
1686 if options.force:
1687 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001688 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689 RunGit(['checkout', '-b', options.newbranch,
1690 Changelist().GetUpstreamBranch()])
1691
1692 # Switch up to the top-level directory, if necessary, in preparation for
1693 # applying the patch.
1694 top = RunGit(['rev-parse', '--show-cdup']).strip()
1695 if top:
1696 os.chdir(top)
1697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001698 # Git patches have a/ at the beginning of source paths. We strip that out
1699 # with a sed script rather than the -p flag to patch so we can feed either
1700 # Git or svn-style patches into the same apply command.
1701 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001702 try:
1703 patch_data = subprocess2.check_output(
1704 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1705 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706 DieWithError('Git patch mungling failed.')
1707 logging.info(patch_data)
1708 # We use "git apply" to apply the patch instead of "patch" so that we can
1709 # pick up file adds.
1710 # The --index flag means: also insert into the index (so we catch adds).
1711 cmd = ['git', 'apply', '--index', '-p0']
1712 if options.reject:
1713 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001714 try:
1715 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1716 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717 DieWithError('Failed to apply the patch')
1718
1719 # If we had an issue, commit the current state and register the issue.
1720 if not options.nocommit:
1721 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1722 cl = Changelist()
1723 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001724 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001725 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 else:
1727 print "Patch applied to index."
1728 return 0
1729
1730
1731def CMDrebase(parser, args):
1732 """rebase current branch on top of svn repo"""
1733 # Provide a wrapper for git svn rebase to help avoid accidental
1734 # git svn dcommit.
1735 # It's the only command that doesn't use parser at all since we just defer
1736 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001737 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738
1739
1740def GetTreeStatus():
1741 """Fetches the tree status and returns either 'open', 'closed',
1742 'unknown' or 'unset'."""
1743 url = settings.GetTreeStatusUrl(error_ok=True)
1744 if url:
1745 status = urllib2.urlopen(url).read().lower()
1746 if status.find('closed') != -1 or status == '0':
1747 return 'closed'
1748 elif status.find('open') != -1 or status == '1':
1749 return 'open'
1750 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751 return 'unset'
1752
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001753
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754def GetTreeStatusReason():
1755 """Fetches the tree status from a json url and returns the message
1756 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001757 url = settings.GetTreeStatusUrl()
1758 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759 connection = urllib2.urlopen(json_url)
1760 status = json.loads(connection.read())
1761 connection.close()
1762 return status['message']
1763
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765def CMDtree(parser, args):
1766 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001767 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 status = GetTreeStatus()
1769 if 'unset' == status:
1770 print 'You must configure your tree status URL by running "git cl config".'
1771 return 2
1772
1773 print "The tree is %s" % status
1774 print
1775 print GetTreeStatusReason()
1776 if status != 'open':
1777 return 1
1778 return 0
1779
1780
maruel@chromium.org15192402012-09-06 12:38:29 +00001781def CMDtry(parser, args):
1782 """Triggers a try job through Rietveld."""
1783 group = optparse.OptionGroup(parser, "Try job options")
1784 group.add_option(
1785 "-b", "--bot", action="append",
1786 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1787 "times to specify multiple builders. ex: "
1788 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1789 "the try server waterfall for the builders name and the tests "
1790 "available. Can also be used to specify gtest_filter, e.g. "
1791 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1792 group.add_option(
1793 "-r", "--revision",
1794 help="Revision to use for the try job; default: the "
1795 "revision will be determined by the try server; see "
1796 "its waterfall for more info")
1797 group.add_option(
1798 "-c", "--clobber", action="store_true", default=False,
1799 help="Force a clobber before building; e.g. don't do an "
1800 "incremental build")
1801 group.add_option(
1802 "--project",
1803 help="Override which project to use. Projects are defined "
1804 "server-side to define what default bot set to use")
1805 group.add_option(
1806 "-t", "--testfilter", action="append", default=[],
1807 help=("Apply a testfilter to all the selected builders. Unless the "
1808 "builders configurations are similar, use multiple "
1809 "--bot <builder>:<test> arguments."))
1810 group.add_option(
1811 "-n", "--name", help="Try job name; default to current branch name")
1812 parser.add_option_group(group)
1813 options, args = parser.parse_args(args)
1814
1815 if args:
1816 parser.error('Unknown arguments: %s' % args)
1817
1818 cl = Changelist()
1819 if not cl.GetIssue():
1820 parser.error('Need to upload first')
1821
1822 if not options.name:
1823 options.name = cl.GetBranch()
1824
1825 # Process --bot and --testfilter.
1826 if not options.bot:
1827 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001828 change = cl.GetChange(
1829 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1830 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001831 options.bot = presubmit_support.DoGetTrySlaves(
1832 change,
1833 change.LocalPaths(),
1834 settings.GetRoot(),
1835 None,
1836 None,
1837 options.verbose,
1838 sys.stdout)
1839 if not options.bot:
1840 parser.error('No default try builder to try, use --bot')
1841
1842 builders_and_tests = {}
1843 for bot in options.bot:
1844 if ':' in bot:
1845 builder, tests = bot.split(':', 1)
1846 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1847 elif ',' in bot:
1848 parser.error('Specify one bot per --bot flag')
1849 else:
1850 builders_and_tests.setdefault(bot, []).append('defaulttests')
1851
1852 if options.testfilter:
1853 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1854 builders_and_tests = dict(
1855 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1856 if t != ['compile'])
1857
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001858 if any('triggered' in b for b in builders_and_tests):
1859 print >> sys.stderr, (
1860 'ERROR You are trying to send a job to a triggered bot. This type of'
1861 ' bot requires an\ninitial job from a parent (usually a builder). '
1862 'Instead send your job to the parent.\n'
1863 'Bot list: %s' % builders_and_tests)
1864 return 1
1865
maruel@chromium.org15192402012-09-06 12:38:29 +00001866 patchset = cl.GetPatchset()
1867 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001868 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001869
1870 cl.RpcServer().trigger_try_jobs(
1871 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1872 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001873 print('Tried jobs on:')
1874 length = max(len(builder) for builder in builders_and_tests)
1875 for builder in sorted(builders_and_tests):
1876 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001877 return 0
1878
1879
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001880@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001882 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001883 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001884 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001885 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001886 return 0
1887
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001888 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001889 if args:
1890 # One arg means set upstream branch.
1891 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1892 cl = Changelist()
1893 print "Upstream branch set to " + cl.GetUpstreamBranch()
1894 else:
1895 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896 return 0
1897
1898
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001899def CMDset_commit(parser, args):
1900 """set the commit bit"""
1901 _, args = parser.parse_args(args)
1902 if args:
1903 parser.error('Unrecognized args: %s' % ' '.join(args))
1904 cl = Changelist()
1905 cl.SetFlag('commit', '1')
1906 return 0
1907
1908
groby@chromium.org411034a2013-02-26 15:12:01 +00001909def CMDset_close(parser, args):
1910 """close the issue"""
1911 _, args = parser.parse_args(args)
1912 if args:
1913 parser.error('Unrecognized args: %s' % ' '.join(args))
1914 cl = Changelist()
1915 # Ensure there actually is an issue to close.
1916 cl.GetDescription()
1917 cl.CloseIssue()
1918 return 0
1919
1920
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921def Command(name):
1922 return getattr(sys.modules[__name__], 'CMD' + name, None)
1923
1924
1925def CMDhelp(parser, args):
1926 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001927 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928 if len(args) == 1:
1929 return main(args + ['--help'])
1930 parser.print_help()
1931 return 0
1932
1933
1934def GenUsage(parser, command):
1935 """Modify an OptParse object with the function's documentation."""
1936 obj = Command(command)
1937 more = getattr(obj, 'usage_more', '')
1938 if command == 'help':
1939 command = '<command>'
1940 else:
1941 # OptParser.description prefer nicely non-formatted strings.
1942 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1943 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1944
1945
1946def main(argv):
1947 """Doesn't parse the arguments here, just find the right subcommand to
1948 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001949 if sys.hexversion < 0x02060000:
1950 print >> sys.stderr, (
1951 '\nYour python version %s is unsupported, please upgrade.\n' %
1952 sys.version.split(' ', 1)[0])
1953 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001954 # Reload settings.
1955 global settings
1956 settings = Settings()
1957
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 # Do it late so all commands are listed.
1959 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1960 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1961 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1962
1963 # Create the option parse and add --verbose support.
1964 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001965 parser.add_option(
1966 '-v', '--verbose', action='count', default=0,
1967 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968 old_parser_args = parser.parse_args
1969 def Parse(args):
1970 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001971 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001972 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001973 elif options.verbose:
1974 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001975 else:
1976 logging.basicConfig(level=logging.WARNING)
1977 return options, args
1978 parser.parse_args = Parse
1979
1980 if argv:
1981 command = Command(argv[0])
1982 if command:
1983 # "fix" the usage and the description now that we know the subcommand.
1984 GenUsage(parser, argv[0])
1985 try:
1986 return command(parser, argv[1:])
1987 except urllib2.HTTPError, e:
1988 if e.code != 500:
1989 raise
1990 DieWithError(
1991 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1992 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1993
1994 # Not a known command. Default to help.
1995 GenUsage(parser, 'help')
1996 return CMDhelp(parser, argv)
1997
1998
1999if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002000 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001 sys.exit(main(sys.argv[1:]))