blob: 43c9d365bd93c3a0d1dabe1d2d6372fd8a8a297d [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
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000011import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000012import logging
13import optparse
14import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000015import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000017import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000022import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023
24try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000025 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026except ImportError:
27 pass
28
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000030from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031from third_party import upload
32import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000033import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000034import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000036import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000038import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000039import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import watchlists
41
maruel@chromium.org0633fb42013-08-16 20:06:14 +000042__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000044DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000045POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000046DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000047GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000048CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000050# Shortcut since it quickly becomes redundant.
51Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000052
maruel@chromium.orgddd59412011-11-30 14:20:38 +000053# Initialized in main()
54settings = None
55
56
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000058 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 sys.exit(1)
60
61
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000064 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000065 except subprocess2.CalledProcessError as e:
66 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000067 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000069 'Command "%s" failed.\n%s' % (
70 ' '.join(args), error_message or e.stdout or ''))
71 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
73
74def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000075 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000076 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000077
78
79def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000080 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000081 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000082 env = os.environ.copy()
83 # 'cat' is a magical git string that disables pagers on all platforms.
84 env['GIT_PAGER'] = 'cat'
85 out, code = subprocess2.communicate(['git'] + args,
86 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000087 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000088 return code, out[0]
89 except ValueError:
90 # When the subprocess fails, it returns None. That triggers a ValueError
91 # when trying to unpack the return value into (out, code).
92 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093
94
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000095def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000096 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000097 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000098 return (version.startswith(prefix) and
99 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000100
101
maruel@chromium.org90541732011-04-01 17:54:18 +0000102def ask_for_data(prompt):
103 try:
104 return raw_input(prompt)
105 except KeyboardInterrupt:
106 # Hide the exception.
107 sys.exit(1)
108
109
iannucci@chromium.org79540052012-10-19 23:15:26 +0000110def git_set_branch_value(key, value):
111 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000112 if not branch:
113 return
114
115 cmd = ['config']
116 if isinstance(value, int):
117 cmd.append('--int')
118 git_key = 'branch.%s.%s' % (branch, key)
119 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000120
121
122def git_get_branch_default(key, default):
123 branch = Changelist().GetBranch()
124 if branch:
125 git_key = 'branch.%s.%s' % (branch, key)
126 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
127 try:
128 return int(stdout.strip())
129 except ValueError:
130 pass
131 return default
132
133
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000134def add_git_similarity(parser):
135 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000136 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137 help='Sets the percentage that a pair of files need to match in order to'
138 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 parser.add_option(
140 '--find-copies', action='store_true',
141 help='Allows git to look for copies.')
142 parser.add_option(
143 '--no-find-copies', action='store_false', dest='find_copies',
144 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000145
146 old_parser_args = parser.parse_args
147 def Parse(args):
148 options, args = old_parser_args(args)
149
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000150 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000151 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000153 print('Note: Saving similarity of %d%% in git config.'
154 % options.similarity)
155 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 options.similarity = max(0, min(options.similarity, 100))
158
159 if options.find_copies is None:
160 options.find_copies = bool(
161 git_get_branch_default('git-find-copies', True))
162 else:
163 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164
165 print('Using %d%% similarity for rename/copy detection. '
166 'Override with --similarity.' % options.similarity)
167
168 return options, args
169 parser.parse_args = Parse
170
171
ukai@chromium.org259e4682012-10-25 07:36:33 +0000172def is_dirty_git_tree(cmd):
173 # Make sure index is up-to-date before running diff-index.
174 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
175 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
176 if dirty:
177 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
178 print 'Uncommitted files: (git diff-index --name-status HEAD)'
179 print dirty[:4096]
180 if len(dirty) > 4096:
181 print '... (run "git diff-index --name-status HEAD" to see full output).'
182 return True
183 return False
184
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000185
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000186def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
187 """Return the corresponding git ref if |base_url| together with |glob_spec|
188 matches the full |url|.
189
190 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
191 """
192 fetch_suburl, as_ref = glob_spec.split(':')
193 if allow_wildcards:
194 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
195 if glob_match:
196 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
197 # "branches/{472,597,648}/src:refs/remotes/svn/*".
198 branch_re = re.escape(base_url)
199 if glob_match.group(1):
200 branch_re += '/' + re.escape(glob_match.group(1))
201 wildcard = glob_match.group(2)
202 if wildcard == '*':
203 branch_re += '([^/]*)'
204 else:
205 # Escape and replace surrounding braces with parentheses and commas
206 # with pipe symbols.
207 wildcard = re.escape(wildcard)
208 wildcard = re.sub('^\\\\{', '(', wildcard)
209 wildcard = re.sub('\\\\,', '|', wildcard)
210 wildcard = re.sub('\\\\}$', ')', wildcard)
211 branch_re += wildcard
212 if glob_match.group(3):
213 branch_re += re.escape(glob_match.group(3))
214 match = re.match(branch_re, url)
215 if match:
216 return re.sub('\*$', match.group(1), as_ref)
217
218 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
219 if fetch_suburl:
220 full_url = base_url + '/' + fetch_suburl
221 else:
222 full_url = base_url
223 if full_url == url:
224 return as_ref
225 return None
226
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000227
iannucci@chromium.org79540052012-10-19 23:15:26 +0000228def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000229 """Prints statistics about the change to the user."""
230 # --no-ext-diff is broken in some versions of Git, so try to work around
231 # this by overriding the environment (but there is still a problem if the
232 # git config key "diff.external" is used).
233 env = os.environ.copy()
234 if 'GIT_EXTERNAL_DIFF' in env:
235 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000236 # 'cat' is a magical git string that disables pagers on all platforms.
237 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000238
239 if find_copies:
240 similarity_options = ['--find-copies-harder', '-l100000',
241 '-C%s' % similarity]
242 else:
243 similarity_options = ['-M%s' % similarity]
244
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000245 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000246 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000247 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000249
250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000251class Settings(object):
252 def __init__(self):
253 self.default_server = None
254 self.cc = None
255 self.root = None
256 self.is_git_svn = None
257 self.svn_branch = None
258 self.tree_status_url = None
259 self.viewvc_url = None
260 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000261 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000262 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000263
264 def LazyUpdateIfNeeded(self):
265 """Updates the settings from a codereview.settings file, if available."""
266 if not self.updated:
267 cr_settings_file = FindCodereviewSettingsFile()
268 if cr_settings_file:
269 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000270 self.updated = True
271 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272 self.updated = True
273
274 def GetDefaultServerUrl(self, error_ok=False):
275 if not self.default_server:
276 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000277 self.default_server = gclient_utils.UpgradeToHttps(
278 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000279 if error_ok:
280 return self.default_server
281 if not self.default_server:
282 error_message = ('Could not find settings file. You must configure '
283 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000284 self.default_server = gclient_utils.UpgradeToHttps(
285 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286 return self.default_server
287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288 def GetRoot(self):
289 if not self.root:
290 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
291 return self.root
292
293 def GetIsGitSvn(self):
294 """Return true if this repo looks like it's using git-svn."""
295 if self.is_git_svn is None:
296 # If you have any "svn-remote.*" config keys, we think you're using svn.
297 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000298 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299 return self.is_git_svn
300
301 def GetSVNBranch(self):
302 if self.svn_branch is None:
303 if not self.GetIsGitSvn():
304 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
305
306 # Try to figure out which remote branch we're based on.
307 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000308 # 1) iterate through our branch history and find the svn URL.
309 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310
311 # regexp matching the git-svn line that contains the URL.
312 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
313
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000314 env = os.environ.copy()
315 # 'cat' is a magical git string that disables pagers on all platforms.
316 env['GIT_PAGER'] = 'cat'
317
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000318 # We don't want to go through all of history, so read a line from the
319 # pipe at a time.
320 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000321 cmd = ['git', 'log', '-100', '--pretty=medium']
322 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000323 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000324 for line in proc.stdout:
325 match = git_svn_re.match(line)
326 if match:
327 url = match.group(1)
328 proc.stdout.close() # Cut pipe.
329 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000330
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000331 if url:
332 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
333 remotes = RunGit(['config', '--get-regexp',
334 r'^svn-remote\..*\.url']).splitlines()
335 for remote in remotes:
336 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000338 remote = match.group(1)
339 base_url = match.group(2)
340 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000341 ['config', 'svn-remote.%s.fetch' % remote],
342 error_ok=True).strip()
343 if fetch_spec:
344 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
345 if self.svn_branch:
346 break
347 branch_spec = RunGit(
348 ['config', 'svn-remote.%s.branches' % remote],
349 error_ok=True).strip()
350 if branch_spec:
351 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
352 if self.svn_branch:
353 break
354 tag_spec = RunGit(
355 ['config', 'svn-remote.%s.tags' % remote],
356 error_ok=True).strip()
357 if tag_spec:
358 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
359 if self.svn_branch:
360 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000361
362 if not self.svn_branch:
363 DieWithError('Can\'t guess svn branch -- try specifying it on the '
364 'command line')
365
366 return self.svn_branch
367
368 def GetTreeStatusUrl(self, error_ok=False):
369 if not self.tree_status_url:
370 error_message = ('You must configure your tree status URL by running '
371 '"git cl config".')
372 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
373 error_ok=error_ok,
374 error_message=error_message)
375 return self.tree_status_url
376
377 def GetViewVCUrl(self):
378 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000379 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000380 return self.viewvc_url
381
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000382 def GetDefaultCCList(self):
383 return self._GetConfig('rietveld.cc', error_ok=True)
384
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000385 def GetDefaultPrivateFlag(self):
386 return self._GetConfig('rietveld.private', error_ok=True)
387
ukai@chromium.orge8077812012-02-03 03:41:46 +0000388 def GetIsGerrit(self):
389 """Return true if this repo is assosiated with gerrit code review system."""
390 if self.is_gerrit is None:
391 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
392 return self.is_gerrit
393
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000394 def GetGitEditor(self):
395 """Return the editor specified in the git config, or None if none is."""
396 if self.git_editor is None:
397 self.git_editor = self._GetConfig('core.editor', error_ok=True)
398 return self.git_editor or None
399
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000400 def _GetConfig(self, param, **kwargs):
401 self.LazyUpdateIfNeeded()
402 return RunGit(['config', param], **kwargs).strip()
403
404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000405def ShortBranchName(branch):
406 """Convert a name like 'refs/heads/foo' to just 'foo'."""
407 return branch.replace('refs/heads/', '')
408
409
410class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000411 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000412 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000413 global settings
414 if not settings:
415 # Happens when git_cl.py is used as a utility library.
416 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000417 settings.GetDefaultServerUrl()
418 self.branchref = branchref
419 if self.branchref:
420 self.branch = ShortBranchName(self.branchref)
421 else:
422 self.branch = None
423 self.rietveld_server = None
424 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000425 self.lookedup_issue = False
426 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000427 self.has_description = False
428 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000429 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000431 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000432 self.cc = None
433 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000434 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000435 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000436
437 def GetCCList(self):
438 """Return the users cc'd on this CL.
439
440 Return is a string suitable for passing to gcl with the --cc flag.
441 """
442 if self.cc is None:
443 base_cc = settings .GetDefaultCCList()
444 more_cc = ','.join(self.watchers)
445 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
446 return self.cc
447
448 def SetWatchers(self, watchers):
449 """Set the list of email addresses that should be cc'd based on the changed
450 files in this CL.
451 """
452 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453
454 def GetBranch(self):
455 """Returns the short branch name, e.g. 'master'."""
456 if not self.branch:
457 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
458 self.branch = ShortBranchName(self.branchref)
459 return self.branch
460
461 def GetBranchRef(self):
462 """Returns the full branch name, e.g. 'refs/heads/master'."""
463 self.GetBranch() # Poke the lazy loader.
464 return self.branchref
465
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000466 @staticmethod
467 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000468 """Returns a tuple containg remote and remote ref,
469 e.g. 'origin', 'refs/heads/master'
470 """
471 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
473 error_ok=True).strip()
474 if upstream_branch:
475 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
476 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000477 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
478 error_ok=True).strip()
479 if upstream_branch:
480 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000482 # Fall back on trying a git-svn upstream branch.
483 if settings.GetIsGitSvn():
484 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000485 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000486 # Else, try to guess the origin remote.
487 remote_branches = RunGit(['branch', '-r']).split()
488 if 'origin/master' in remote_branches:
489 # Fall back on origin/master if it exits.
490 remote = 'origin'
491 upstream_branch = 'refs/heads/master'
492 elif 'origin/trunk' in remote_branches:
493 # Fall back on origin/trunk if it exists. Generally a shared
494 # git-svn clone
495 remote = 'origin'
496 upstream_branch = 'refs/heads/trunk'
497 else:
498 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000499Either pass complete "git diff"-style arguments, like
500 git cl upload origin/master
501or verify this branch is set up to track another (via the --track argument to
502"git checkout -b ...").""")
503
504 return remote, upstream_branch
505
506 def GetUpstreamBranch(self):
507 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000508 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000509 if remote is not '.':
510 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
511 self.upstream_branch = upstream_branch
512 return self.upstream_branch
513
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000514 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000515 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 remote, branch = None, self.GetBranch()
517 seen_branches = set()
518 while branch not in seen_branches:
519 seen_branches.add(branch)
520 remote, branch = self.FetchUpstreamTuple(branch)
521 branch = ShortBranchName(branch)
522 if remote != '.' or branch.startswith('refs/remotes'):
523 break
524 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000525 remotes = RunGit(['remote'], error_ok=True).split()
526 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000527 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000528 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000529 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000530 logging.warning('Could not determine which remote this change is '
531 'associated with, so defaulting to "%s". This may '
532 'not be what you want. You may prevent this message '
533 'by running "git svn info" as documented here: %s',
534 self._remote,
535 GIT_INSTRUCTIONS_URL)
536 else:
537 logging.warn('Could not determine which remote this change is '
538 'associated with. You may prevent this message by '
539 'running "git svn info" as documented here: %s',
540 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000541 branch = 'HEAD'
542 if branch.startswith('refs/remotes'):
543 self._remote = (remote, branch)
544 else:
545 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000546 return self._remote
547
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000548 def GitSanityChecks(self, upstream_git_obj):
549 """Checks git repo status and ensures diff is from local commits."""
550
551 # Verify the commit we're diffing against is in our current branch.
552 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
553 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
554 if upstream_sha != common_ancestor:
555 print >> sys.stderr, (
556 'ERROR: %s is not in the current branch. You may need to rebase '
557 'your tracking branch' % upstream_sha)
558 return False
559
560 # List the commits inside the diff, and verify they are all local.
561 commits_in_diff = RunGit(
562 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
563 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
564 remote_branch = remote_branch.strip()
565 if code != 0:
566 _, remote_branch = self.GetRemoteBranch()
567
568 commits_in_remote = RunGit(
569 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
570
571 common_commits = set(commits_in_diff) & set(commits_in_remote)
572 if common_commits:
573 print >> sys.stderr, (
574 'ERROR: Your diff contains %d commits already in %s.\n'
575 'Run "git log --oneline %s..HEAD" to get a list of commits in '
576 'the diff. If you are using a custom git flow, you can override'
577 ' the reference used for this check with "git config '
578 'gitcl.remotebranch <git-ref>".' % (
579 len(common_commits), remote_branch, upstream_git_obj))
580 return False
581 return True
582
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000583 def GetGitBaseUrlFromConfig(self):
584 """Return the configured base URL from branch.<branchname>.baseurl.
585
586 Returns None if it is not set.
587 """
588 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
589 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000590
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591 def GetRemoteUrl(self):
592 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
593
594 Returns None if there is no remote.
595 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000596 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
598
599 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000600 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000601 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000603 self.issue = int(issue) or None if issue else None
604 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 return self.issue
606
607 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000608 if not self.rietveld_server:
609 # If we're on a branch then get the server potentially associated
610 # with that branch.
611 if self.GetIssue():
612 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
613 ['config', self._RietveldServer()], error_ok=True).strip())
614 if not self.rietveld_server:
615 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 return self.rietveld_server
617
618 def GetIssueURL(self):
619 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000620 if not self.GetIssue():
621 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
623
624 def GetDescription(self, pretty=False):
625 if not self.has_description:
626 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000627 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000628 try:
629 self.description = self.RpcServer().get_description(issue).strip()
630 except urllib2.HTTPError, e:
631 if e.code == 404:
632 DieWithError(
633 ('\nWhile fetching the description for issue %d, received a '
634 '404 (not found)\n'
635 'error. It is likely that you deleted this '
636 'issue on the server. If this is the\n'
637 'case, please run\n\n'
638 ' git cl issue 0\n\n'
639 'to clear the association with the deleted issue. Then run '
640 'this command again.') % issue)
641 else:
642 DieWithError(
643 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644 self.has_description = True
645 if pretty:
646 wrapper = textwrap.TextWrapper()
647 wrapper.initial_indent = wrapper.subsequent_indent = ' '
648 return wrapper.fill(self.description)
649 return self.description
650
651 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000652 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000653 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000654 patchset = RunGit(['config', self._PatchsetSetting()],
655 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000656 self.patchset = int(patchset) or None if patchset else None
657 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658 return self.patchset
659
660 def SetPatchset(self, patchset):
661 """Set this branch's patchset. If patchset=0, clears the patchset."""
662 if patchset:
663 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000664 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665 else:
666 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000667 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000668 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000670 def GetMostRecentPatchset(self):
671 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000672
673 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000674 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000675 '/download/issue%s_%s.diff' % (issue, patchset))
676
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000677 def GetIssueProperties(self):
678 if self._props is None:
679 issue = self.GetIssue()
680 if not issue:
681 self._props = {}
682 else:
683 self._props = self.RpcServer().get_issue_properties(issue, True)
684 return self._props
685
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000686 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000687 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000688
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689 def SetIssue(self, issue):
690 """Set this branch's issue. If issue=0, clears the issue."""
691 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000692 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000693 RunGit(['config', self._IssueSetting(), str(issue)])
694 if self.rietveld_server:
695 RunGit(['config', self._RietveldServer(), self.rietveld_server])
696 else:
697 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000698 self.issue = None
699 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000701 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000702 if not self.GitSanityChecks(upstream_branch):
703 DieWithError('\nGit sanity check failure')
704
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000705 env = os.environ.copy()
706 # 'cat' is a magical git string that disables pagers on all platforms.
707 env['GIT_PAGER'] = 'cat'
708
709 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000710 if not root:
711 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000712 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000713
714 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000715 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000716 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000717 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000718 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000719 except subprocess2.CalledProcessError:
720 DieWithError(
721 ('\nFailed to diff against upstream branch %s!\n\n'
722 'This branch probably doesn\'t exist anymore. To reset the\n'
723 'tracking branch, please run\n'
724 ' git branch --set-upstream %s trunk\n'
725 'replacing trunk with origin/master or the relevant branch') %
726 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000727
maruel@chromium.org52424302012-08-29 15:14:30 +0000728 issue = self.GetIssue()
729 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000730 if issue:
731 description = self.GetDescription()
732 else:
733 # If the change was never uploaded, use the log messages of all commits
734 # up to the branch point, as git cl upload will prefill the description
735 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000736 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000737 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000738 '%s...' % (upstream_branch)],
739 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000740
741 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000742 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000743 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000744 name,
745 description,
746 absroot,
747 files,
748 issue,
749 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000750 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000751
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000752 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000753 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000754
755 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000756 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000757 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000758 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000759 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000760 except presubmit_support.PresubmitFailure, e:
761 DieWithError(
762 ('%s\nMaybe your depot_tools is out of date?\n'
763 'If all fails, contact maruel@') % e)
764
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000765 def UpdateDescription(self, description):
766 self.description = description
767 return self.RpcServer().update_description(
768 self.GetIssue(), self.description)
769
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000771 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000772 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000774 def SetFlag(self, flag, value):
775 """Patchset must match."""
776 if not self.GetPatchset():
777 DieWithError('The patchset needs to match. Send another patchset.')
778 try:
779 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000780 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000781 except urllib2.HTTPError, e:
782 if e.code == 404:
783 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
784 if e.code == 403:
785 DieWithError(
786 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
787 'match?') % (self.GetIssue(), self.GetPatchset()))
788 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000790 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 """Returns an upload.RpcServer() to access this review's rietveld instance.
792 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000793 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000794 self._rpc_server = rietveld.CachingRietveld(
795 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000796 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797
798 def _IssueSetting(self):
799 """Return the git setting that stores this change's issue."""
800 return 'branch.%s.rietveldissue' % self.GetBranch()
801
802 def _PatchsetSetting(self):
803 """Return the git setting that stores this change's most recent patchset."""
804 return 'branch.%s.rietveldpatchset' % self.GetBranch()
805
806 def _RietveldServer(self):
807 """Returns the git setting that stores this change's rietveld server."""
808 return 'branch.%s.rietveldserver' % self.GetBranch()
809
810
811def GetCodereviewSettingsInteractively():
812 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000813 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 server = settings.GetDefaultServerUrl(error_ok=True)
815 prompt = 'Rietveld server (host[:port])'
816 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000817 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 if not server and not newserver:
819 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000820 if newserver:
821 newserver = gclient_utils.UpgradeToHttps(newserver)
822 if newserver != server:
823 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000825 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 prompt = caption
827 if initial:
828 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000829 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830 if new_val == 'x':
831 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000832 elif new_val:
833 if is_url:
834 new_val = gclient_utils.UpgradeToHttps(new_val)
835 if new_val != initial:
836 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000838 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000839 SetProperty(settings.GetDefaultPrivateFlag(),
840 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000842 'tree-status-url', False)
843 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
845 # TODO: configure a default branch to diff against, rather than this
846 # svn-based hackery.
847
848
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000849class ChangeDescription(object):
850 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000851 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000852 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000853
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000854 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000855 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000856
agable@chromium.org42c20792013-09-12 17:34:49 +0000857 @property # www.logilab.org/ticket/89786
858 def description(self): # pylint: disable=E0202
859 return '\n'.join(self._description_lines)
860
861 def set_description(self, desc):
862 if isinstance(desc, basestring):
863 lines = desc.splitlines()
864 else:
865 lines = [line.rstrip() for line in desc]
866 while lines and not lines[0]:
867 lines.pop(0)
868 while lines and not lines[-1]:
869 lines.pop(-1)
870 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000871
872 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000873 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000874 assert isinstance(reviewers, list), reviewers
875 if not reviewers:
876 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000877 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000878
agable@chromium.org42c20792013-09-12 17:34:49 +0000879 # Get the set of R= and TBR= lines and remove them from the desciption.
880 regexp = re.compile(self.R_LINE)
881 matches = [regexp.match(line) for line in self._description_lines]
882 new_desc = [l for i, l in enumerate(self._description_lines)
883 if not matches[i]]
884 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000885
agable@chromium.org42c20792013-09-12 17:34:49 +0000886 # Construct new unified R= and TBR= lines.
887 r_names = []
888 tbr_names = []
889 for match in matches:
890 if not match:
891 continue
892 people = cleanup_list([match.group(2).strip()])
893 if match.group(1) == 'TBR':
894 tbr_names.extend(people)
895 else:
896 r_names.extend(people)
897 for name in r_names:
898 if name not in reviewers:
899 reviewers.append(name)
900 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
901 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
902
903 # Put the new lines in the description where the old first R= line was.
904 line_loc = next((i for i, match in enumerate(matches) if match), -1)
905 if 0 <= line_loc < len(self._description_lines):
906 if new_tbr_line:
907 self._description_lines.insert(line_loc, new_tbr_line)
908 if new_r_line:
909 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000910 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000911 if new_r_line:
912 self.append_footer(new_r_line)
913 if new_tbr_line:
914 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000915
916 def prompt(self):
917 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000918 self.set_description([
919 '# Enter a description of the change.',
920 '# This will be displayed on the codereview site.',
921 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000922 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000923 '--------------------',
924 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000925
agable@chromium.org42c20792013-09-12 17:34:49 +0000926 regexp = re.compile(self.BUG_LINE)
927 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000928 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000929 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000930 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000931 if not content:
932 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000933 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000934
935 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000936 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
937 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000938 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000939 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000940
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000941 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 if self._description_lines:
943 # Add an empty line if either the last line or the new line isn't a tag.
944 last_line = self._description_lines[-1]
945 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
946 not presubmit_support.Change.TAG_LINE_RE.match(line)):
947 self._description_lines.append('')
948 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000949
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000950 def get_reviewers(self):
951 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000952 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
953 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000954 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000955
956
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000957def get_approving_reviewers(props):
958 """Retrieves the reviewers that approved a CL from the issue properties with
959 messages.
960
961 Note that the list may contain reviewers that are not committer, thus are not
962 considered by the CQ.
963 """
964 return sorted(
965 set(
966 message['sender']
967 for message in props['messages']
968 if message['approval'] and message['sender'] in props['reviewers']
969 )
970 )
971
972
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973def FindCodereviewSettingsFile(filename='codereview.settings'):
974 """Finds the given file starting in the cwd and going up.
975
976 Only looks up to the top of the repository unless an
977 'inherit-review-settings-ok' file exists in the root of the repository.
978 """
979 inherit_ok_file = 'inherit-review-settings-ok'
980 cwd = os.getcwd()
981 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
982 if os.path.isfile(os.path.join(root, inherit_ok_file)):
983 root = '/'
984 while True:
985 if filename in os.listdir(cwd):
986 if os.path.isfile(os.path.join(cwd, filename)):
987 return open(os.path.join(cwd, filename))
988 if cwd == root:
989 break
990 cwd = os.path.dirname(cwd)
991
992
993def LoadCodereviewSettingsFromFile(fileobj):
994 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000995 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997 def SetProperty(name, setting, unset_error_ok=False):
998 fullname = 'rietveld.' + name
999 if setting in keyvals:
1000 RunGit(['config', fullname, keyvals[setting]])
1001 else:
1002 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1003
1004 SetProperty('server', 'CODE_REVIEW_SERVER')
1005 # Only server setting is required. Other settings can be absent.
1006 # In that case, we ignore errors raised during option deletion attempt.
1007 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001008 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1010 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1011
ukai@chromium.orge8077812012-02-03 03:41:46 +00001012 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
1013 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
1014 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1017 #should be of the form
1018 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1019 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1020 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1021 keyvals['ORIGIN_URL_CONFIG']])
1022
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001024def urlretrieve(source, destination):
1025 """urllib is broken for SSL connections via a proxy therefore we
1026 can't use urllib.urlretrieve()."""
1027 with open(destination, 'w') as f:
1028 f.write(urllib2.urlopen(source).read())
1029
1030
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001031def DownloadHooks(force):
1032 """downloads hooks
1033
1034 Args:
1035 force: True to update hooks. False to install hooks if not present.
1036 """
1037 if not settings.GetIsGerrit():
1038 return
1039 server_url = settings.GetDefaultServerUrl()
1040 src = '%s/tools/hooks/commit-msg' % server_url
1041 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1042 if not os.access(dst, os.X_OK):
1043 if os.path.exists(dst):
1044 if not force:
1045 return
1046 os.remove(dst)
1047 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001048 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001049 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1050 except Exception:
1051 if os.path.exists(dst):
1052 os.remove(dst)
1053 DieWithError('\nFailed to download hooks from %s' % src)
1054
1055
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001056@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001058 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001060 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 if len(args) == 0:
1062 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001063 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 return 0
1065
1066 url = args[0]
1067 if not url.endswith('codereview.settings'):
1068 url = os.path.join(url, 'codereview.settings')
1069
1070 # Load code review settings and download hooks (if available).
1071 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001072 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001073 return 0
1074
1075
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001076def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001077 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001078 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1079 branch = ShortBranchName(branchref)
1080 _, args = parser.parse_args(args)
1081 if not args:
1082 print("Current base-url:")
1083 return RunGit(['config', 'branch.%s.base-url' % branch],
1084 error_ok=False).strip()
1085 else:
1086 print("Setting base-url to %s" % args[0])
1087 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1088 error_ok=False).strip()
1089
1090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001092 """Show status of changelists.
1093
1094 Colors are used to tell the state of the CL unless --fast is used:
1095 - Green LGTM'ed
1096 - Blue waiting for review
1097 - Yellow waiting for you to reply to review
1098 - Red not sent for review or broken
1099 - Cyan was committed, branch can be deleted
1100
1101 Also see 'git cl comments'.
1102 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 parser.add_option('--field',
1104 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001105 parser.add_option('-f', '--fast', action='store_true',
1106 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001107 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001108 if args:
1109 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001112 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113 if options.field.startswith('desc'):
1114 print cl.GetDescription()
1115 elif options.field == 'id':
1116 issueid = cl.GetIssue()
1117 if issueid:
1118 print issueid
1119 elif options.field == 'patch':
1120 patchset = cl.GetPatchset()
1121 if patchset:
1122 print patchset
1123 elif options.field == 'url':
1124 url = cl.GetIssueURL()
1125 if url:
1126 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001127 return 0
1128
1129 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1130 if not branches:
1131 print('No local branch found.')
1132 return 0
1133
1134 changes = (Changelist(branchref=b) for b in branches.splitlines())
1135 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1136 alignment = max(5, max(len(b) for b in branches))
1137 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001138 # Adhoc thread pool to request data concurrently.
1139 output = Queue.Queue()
1140
1141 # Silence upload.py otherwise it becomes unweldly.
1142 upload.verbosity = 0
1143
1144 if not options.fast:
1145 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001146 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 c = Changelist(branchref=b)
1148 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001149 props = {}
1150 r = None
1151 if i:
1152 try:
1153 props = c.GetIssueProperties()
1154 r = c.GetApprovingReviewers() if i else None
1155 except urllib2.HTTPError:
1156 # The issue probably doesn't exist anymore.
1157 i += ' (broken)'
1158
1159 msgs = props.get('messages') or []
1160
1161 if not i:
1162 color = Fore.WHITE
1163 elif props.get('closed'):
1164 # Issue is closed.
1165 color = Fore.CYAN
1166 elif r:
1167 # Was LGTM'ed.
1168 color = Fore.GREEN
1169 elif not msgs:
1170 # No message was sent.
1171 color = Fore.RED
1172 elif msgs[-1]['sender'] != props.get('owner_email'):
1173 color = Fore.YELLOW
1174 else:
1175 color = Fore.BLUE
1176 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001177
1178 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1179 for t in threads:
1180 t.daemon = True
1181 t.start()
1182 else:
1183 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1184 for b in branches:
1185 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001186 url = c.GetIssueURL()
1187 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001188
1189 tmp = {}
1190 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001191 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001192 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001193 b, i, color = output.get()
1194 tmp[b] = (i, color)
1195 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001196 reset = Fore.RESET
1197 if not sys.stdout.isatty():
1198 color = ''
1199 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001200 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001201 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001202
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001203 cl = Changelist()
1204 print
1205 print 'Current branch:',
1206 if not cl.GetIssue():
1207 print 'no issue assigned.'
1208 return 0
1209 print cl.GetBranch()
1210 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1211 print 'Issue description:'
1212 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 return 0
1214
1215
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001216def colorize_CMDstatus_doc():
1217 """To be called once in main() to add colors to git cl status help."""
1218 colors = [i for i in dir(Fore) if i[0].isupper()]
1219
1220 def colorize_line(line):
1221 for color in colors:
1222 if color in line.upper():
1223 # Extract whitespaces first and the leading '-'.
1224 indent = len(line) - len(line.lstrip(' ')) + 1
1225 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1226 return line
1227
1228 lines = CMDstatus.__doc__.splitlines()
1229 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1230
1231
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001232@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001234 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
1236 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001237 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001238 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
1240 cl = Changelist()
1241 if len(args) > 0:
1242 try:
1243 issue = int(args[0])
1244 except ValueError:
1245 DieWithError('Pass a number to set the issue or none to list it.\n'
1246 'Maybe you want to run git cl status?')
1247 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001248 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 return 0
1250
1251
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001252def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001253 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001254 (_, args) = parser.parse_args(args)
1255 if args:
1256 parser.error('Unsupported argument: %s' % args)
1257
1258 cl = Changelist()
1259 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001260 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001261 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001262 if message['disapproval']:
1263 color = Fore.RED
1264 elif message['approval']:
1265 color = Fore.GREEN
1266 elif message['sender'] == data['owner_email']:
1267 color = Fore.MAGENTA
1268 else:
1269 color = Fore.BLUE
1270 print '\n%s%s %s%s' % (
1271 color, message['date'].split('.', 1)[0], message['sender'],
1272 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001273 if message['text'].strip():
1274 print '\n'.join(' ' + l for l in message['text'].splitlines())
1275 return 0
1276
1277
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001278def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001279 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001280 cl = Changelist()
1281 if not cl.GetIssue():
1282 DieWithError('This branch has no associated changelist.')
1283 description = ChangeDescription(cl.GetDescription())
1284 description.prompt()
1285 cl.UpdateDescription(description.description)
1286 return 0
1287
1288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289def CreateDescriptionFromLog(args):
1290 """Pulls out the commit log to use as a base for the CL description."""
1291 log_args = []
1292 if len(args) == 1 and not args[0].endswith('.'):
1293 log_args = [args[0] + '..']
1294 elif len(args) == 1 and args[0].endswith('...'):
1295 log_args = [args[0][:-1]]
1296 elif len(args) == 2:
1297 log_args = [args[0] + '..' + args[1]]
1298 else:
1299 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001300 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301
1302
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001304 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001305 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001307 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001308 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309 (options, args) = parser.parse_args(args)
1310
ukai@chromium.org259e4682012-10-25 07:36:33 +00001311 if not options.force and is_dirty_git_tree('presubmit'):
1312 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 return 1
1314
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001315 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 if args:
1317 base_branch = args[0]
1318 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001319 # Default to diffing against the common ancestor of the upstream branch.
1320 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001322 cl.RunHook(
1323 committing=not options.upload,
1324 may_prompt=False,
1325 verbose=options.verbose,
1326 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001327 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328
1329
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001330def AddChangeIdToCommitMessage(options, args):
1331 """Re-commits using the current message, assumes the commit hook is in
1332 place.
1333 """
1334 log_desc = options.message or CreateDescriptionFromLog(args)
1335 git_command = ['commit', '--amend', '-m', log_desc]
1336 RunGit(git_command)
1337 new_log_desc = CreateDescriptionFromLog(args)
1338 if CHANGE_ID in new_log_desc:
1339 print 'git-cl: Added Change-Id to commit message.'
1340 else:
1341 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1342
1343
ukai@chromium.orge8077812012-02-03 03:41:46 +00001344def GerritUpload(options, args, cl):
1345 """upload the current branch to gerrit."""
1346 # We assume the remote called "origin" is the one we want.
1347 # It is probably not worthwhile to support different workflows.
1348 remote = 'origin'
1349 branch = 'master'
1350 if options.target_branch:
1351 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001353 change_desc = ChangeDescription(
1354 options.message or CreateDescriptionFromLog(args))
1355 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001356 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001358 if CHANGE_ID not in change_desc.description:
1359 AddChangeIdToCommitMessage(options, args)
1360 if options.reviewers:
1361 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
ukai@chromium.orge8077812012-02-03 03:41:46 +00001363 receive_options = []
1364 cc = cl.GetCCList().split(',')
1365 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001366 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001367 cc = filter(None, cc)
1368 if cc:
1369 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001370 if change_desc.get_reviewers():
1371 receive_options.extend(
1372 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373
ukai@chromium.orge8077812012-02-03 03:41:46 +00001374 git_command = ['push']
1375 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001376 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001377 ' '.join(receive_options))
1378 git_command += [remote, 'HEAD:refs/for/' + branch]
1379 RunGit(git_command)
1380 # TODO(ukai): parse Change-Id: and set issue number?
1381 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001382
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383
ukai@chromium.orge8077812012-02-03 03:41:46 +00001384def RietveldUpload(options, args, cl):
1385 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1387 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388 if options.emulate_svn_auto_props:
1389 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390
1391 change_desc = None
1392
1393 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001394 if options.title:
1395 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001396 if options.message:
1397 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001398 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 print ("This branch is associated with issue %s. "
1400 "Adding patch to that issue." % cl.GetIssue())
1401 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001402 if options.title:
1403 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001404 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001405 change_desc = ChangeDescription(message)
1406 if options.reviewers:
1407 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001408 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001409 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001410
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001411 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 print "Description is empty; aborting."
1413 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001414
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001415 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001416 if change_desc.get_reviewers():
1417 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001418 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001419 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001420 DieWithError("Must specify reviewers to send email.")
1421 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001422 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001423 if cc:
1424 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001426 if options.private or settings.GetDefaultPrivateFlag() == "True":
1427 upload_args.append('--private')
1428
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001429 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001430 if not options.find_copies:
1431 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 # Include the upstream repo's URL in the change -- this is useful for
1434 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001435 remote_url = cl.GetGitBaseUrlFromConfig()
1436 if not remote_url:
1437 if settings.GetIsGitSvn():
1438 # URL is dependent on the current directory.
1439 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1440 if data:
1441 keys = dict(line.split(': ', 1) for line in data.splitlines()
1442 if ': ' in line)
1443 remote_url = keys.get('URL', None)
1444 else:
1445 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1446 remote_url = (cl.GetRemoteUrl() + '@'
1447 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448 if remote_url:
1449 upload_args.extend(['--base_url', remote_url])
1450
1451 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001452 upload_args = ['upload'] + upload_args + args
1453 logging.info('upload.RealMain(%s)', upload_args)
1454 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001455 issue = int(issue)
1456 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001457 except KeyboardInterrupt:
1458 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 except:
1460 # If we got an exception after the user typed a description for their
1461 # change, back up the description before re-raising.
1462 if change_desc:
1463 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1464 print '\nGot exception while uploading -- saving description to %s\n' \
1465 % backup_path
1466 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001467 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 backup_file.close()
1469 raise
1470
1471 if not cl.GetIssue():
1472 cl.SetIssue(issue)
1473 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001474
1475 if options.use_commit_queue:
1476 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 return 0
1478
1479
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001480def cleanup_list(l):
1481 """Fixes a list so that comma separated items are put as individual items.
1482
1483 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1484 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1485 """
1486 items = sum((i.split(',') for i in l), [])
1487 stripped_items = (i.strip() for i in items)
1488 return sorted(filter(None, stripped_items))
1489
1490
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001491@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001492def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001493 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001494 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1495 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001496 parser.add_option('--bypass-watchlists', action='store_true',
1497 dest='bypass_watchlists',
1498 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001499 parser.add_option('-f', action='store_true', dest='force',
1500 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001501 parser.add_option('-m', dest='message', help='message for patchset')
1502 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001503 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001504 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001505 help='reviewer email addresses')
1506 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001507 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001508 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001509 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001510 help='send email to reviewer immediately')
1511 parser.add_option("--emulate_svn_auto_props", action="store_true",
1512 dest="emulate_svn_auto_props",
1513 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001514 parser.add_option('-c', '--use-commit-queue', action='store_true',
1515 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001516 parser.add_option('--private', action='store_true',
1517 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001518 parser.add_option('--target_branch',
1519 help='When uploading to gerrit, remote branch to '
1520 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001521 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001522 (options, args) = parser.parse_args(args)
1523
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001524 if options.target_branch and not settings.GetIsGerrit():
1525 parser.error('Use --target_branch for non gerrit repository.')
1526
ukai@chromium.org259e4682012-10-25 07:36:33 +00001527 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001528 return 1
1529
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001530 options.reviewers = cleanup_list(options.reviewers)
1531 options.cc = cleanup_list(options.cc)
1532
ukai@chromium.orge8077812012-02-03 03:41:46 +00001533 cl = Changelist()
1534 if args:
1535 # TODO(ukai): is it ok for gerrit case?
1536 base_branch = args[0]
1537 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001538 # Default to diffing against common ancestor of upstream branch
1539 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001540 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001542 # Apply watchlists on upload.
1543 change = cl.GetChange(base_branch, None)
1544 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1545 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001546 if not options.bypass_watchlists:
1547 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001548
ukai@chromium.orge8077812012-02-03 03:41:46 +00001549 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001550 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001551 may_prompt=not options.force,
1552 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001553 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001554 if not hook_results.should_continue():
1555 return 1
1556 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001557 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001558
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001559 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001560 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001561 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001562 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001563 print ('The last upload made from this repository was patchset #%d but '
1564 'the most recent patchset on the server is #%d.'
1565 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001566 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1567 'from another machine or branch the patch you\'re uploading now '
1568 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001569 ask_for_data('About to upload; enter to confirm.')
1570
iannucci@chromium.org79540052012-10-19 23:15:26 +00001571 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001572 if settings.GetIsGerrit():
1573 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001574 ret = RietveldUpload(options, args, cl)
1575 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001576 git_set_branch_value('last-upload-hash',
1577 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001578
1579 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001580
1581
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001582def IsSubmoduleMergeCommit(ref):
1583 # When submodules are added to the repo, we expect there to be a single
1584 # non-git-svn merge commit at remote HEAD with a signature comment.
1585 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001586 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001587 return RunGit(cmd) != ''
1588
1589
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001590def SendUpstream(parser, args, cmd):
1591 """Common code for CmdPush and CmdDCommit
1592
1593 Squashed commit into a single.
1594 Updates changelog with metadata (e.g. pointer to review).
1595 Pushes/dcommits the code upstream.
1596 Updates review and closes.
1597 """
1598 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1599 help='bypass upload presubmit hook')
1600 parser.add_option('-m', dest='message',
1601 help="override review description")
1602 parser.add_option('-f', action='store_true', dest='force',
1603 help="force yes to questions (don't prompt)")
1604 parser.add_option('-c', dest='contributor',
1605 help="external contributor for patch (appended to " +
1606 "description and used as author for git). Should be " +
1607 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001608 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 (options, args) = parser.parse_args(args)
1610 cl = Changelist()
1611
1612 if not args or cmd == 'push':
1613 # Default to merging against our best guess of the upstream branch.
1614 args = [cl.GetUpstreamBranch()]
1615
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001616 if options.contributor:
1617 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1618 print "Please provide contibutor as 'First Last <email@example.com>'"
1619 return 1
1620
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001622 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623
ukai@chromium.org259e4682012-10-25 07:36:33 +00001624 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001625 return 1
1626
1627 # This rev-list syntax means "show all commits not in my branch that
1628 # are in base_branch".
1629 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1630 base_branch]).splitlines()
1631 if upstream_commits:
1632 print ('Base branch "%s" has %d commits '
1633 'not in this branch.' % (base_branch, len(upstream_commits)))
1634 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1635 return 1
1636
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001637 # This is the revision `svn dcommit` will commit on top of.
1638 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1639 '--pretty=format:%H'])
1640
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001642 # If the base_head is a submodule merge commit, the first parent of the
1643 # base_head should be a git-svn commit, which is what we're interested in.
1644 base_svn_head = base_branch
1645 if base_has_submodules:
1646 base_svn_head += '^1'
1647
1648 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 if extra_commits:
1650 print ('This branch has %d additional commits not upstreamed yet.'
1651 % len(extra_commits.splitlines()))
1652 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1653 'before attempting to %s.' % (base_branch, cmd))
1654 return 1
1655
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001656 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001657 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001658 author = None
1659 if options.contributor:
1660 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001661 hook_results = cl.RunHook(
1662 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001663 may_prompt=not options.force,
1664 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001665 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001666 if not hook_results.should_continue():
1667 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001668
1669 if cmd == 'dcommit':
1670 # Check the tree status if the tree status URL is set.
1671 status = GetTreeStatus()
1672 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001673 print('The tree is closed. Please wait for it to reopen. Use '
1674 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675 return 1
1676 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001677 print('Unable to determine tree status. Please verify manually and '
1678 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001679 else:
1680 breakpad.SendStack(
1681 'GitClHooksBypassedCommit',
1682 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001683 (cl.GetRietveldServer(), cl.GetIssue()),
1684 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001685
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001686 change_desc = ChangeDescription(options.message)
1687 if not change_desc.description and cl.GetIssue():
1688 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001690 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001691 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001692 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001693 else:
1694 print 'No description set.'
1695 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1696 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001697
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001698 # Keep a separate copy for the commit message, because the commit message
1699 # contains the link to the Rietveld issue, while the Rietveld message contains
1700 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001701 # Keep a separate copy for the commit message.
1702 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001703 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001704
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001705 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001706 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001707 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001709 commit_desc.append_footer('Patch from %s.' % options.contributor)
1710
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001711 print('Description:')
1712 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713
1714 branches = [base_branch, cl.GetBranchRef()]
1715 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001716 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001717 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001719 # We want to squash all this branch's commits into one commit with the proper
1720 # description. We do this by doing a "reset --soft" to the base branch (which
1721 # keeps the working copy the same), then dcommitting that. If origin/master
1722 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1723 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001725 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1726 # Delete the branches if they exist.
1727 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1728 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1729 result = RunGitWithCode(showref_cmd)
1730 if result[0] == 0:
1731 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732
1733 # We might be in a directory that's present in this branch but not in the
1734 # trunk. Move up to the top of the tree so that git commands that expect a
1735 # valid CWD won't fail after we check out the merge branch.
1736 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1737 if rel_base_path:
1738 os.chdir(rel_base_path)
1739
1740 # Stuff our change into the merge branch.
1741 # We wrap in a try...finally block so if anything goes wrong,
1742 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001743 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001744 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001745 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1746 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001747 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001748 RunGit(
1749 [
1750 'commit', '--author', options.contributor,
1751 '-m', commit_desc.description,
1752 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001754 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001755 if base_has_submodules:
1756 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1757 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1758 RunGit(['checkout', CHERRY_PICK_BRANCH])
1759 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760 if cmd == 'push':
1761 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001762 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763 retcode, output = RunGitWithCode(
1764 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1765 logging.debug(output)
1766 else:
1767 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001768 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001769 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001770 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771 finally:
1772 # And then swap back to the original branch and clean up.
1773 RunGit(['checkout', '-q', cl.GetBranch()])
1774 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001775 if base_has_submodules:
1776 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777
1778 if cl.GetIssue():
1779 if cmd == 'dcommit' and 'Committed r' in output:
1780 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1781 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001782 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1783 for l in output.splitlines(False))
1784 match = filter(None, match)
1785 if len(match) != 1:
1786 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1787 output)
1788 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789 else:
1790 return 1
1791 viewvc_url = settings.GetViewVCUrl()
1792 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001793 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001794 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001795 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 print ('Closing issue '
1797 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001798 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001799 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001800 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001801 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001802 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001803 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1804 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001805 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001806
1807 if retcode == 0:
1808 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1809 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001810 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001811
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812 return 0
1813
1814
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001815@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001816def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001817 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001819 message = """This doesn't appear to be an SVN repository.
1820If your project has a git mirror with an upstream SVN master, you probably need
1821to run 'git svn init', see your project's git mirror documentation.
1822If your project has a true writeable upstream repository, you probably want
1823to run 'git cl push' instead.
1824Choose wisely, if you get this wrong, your commit might appear to succeed but
1825will instead be silently ignored."""
1826 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001827 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828 return SendUpstream(parser, args, 'dcommit')
1829
1830
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001831@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001833 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834 if settings.GetIsGitSvn():
1835 print('This appears to be an SVN repository.')
1836 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001837 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838 return SendUpstream(parser, args, 'push')
1839
1840
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001841@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842def CMDpatch(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001843 """Patchs in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844 parser.add_option('-b', dest='newbranch',
1845 help='create a new branch off trunk for the patch')
1846 parser.add_option('-f', action='store_true', dest='force',
1847 help='with -b, clobber any existing branch')
1848 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001849 help='failed patches spew .rej files rather than '
1850 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1852 help="don't commit after patch applies")
1853 (options, args) = parser.parse_args(args)
1854 if len(args) != 1:
1855 parser.print_help()
1856 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001857 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001858
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001859 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001860 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001861
maruel@chromium.org52424302012-08-29 15:14:30 +00001862 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001864 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001865 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001866 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001867 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001868 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001869 # Assume it's a URL to the patch. Default to https.
1870 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001871 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001872 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001873 DieWithError('Must pass an issue ID or full URL for '
1874 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001875 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001876 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001877 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001878
1879 if options.newbranch:
1880 if options.force:
1881 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001882 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001883 RunGit(['checkout', '-b', options.newbranch,
1884 Changelist().GetUpstreamBranch()])
1885
1886 # Switch up to the top-level directory, if necessary, in preparation for
1887 # applying the patch.
1888 top = RunGit(['rev-parse', '--show-cdup']).strip()
1889 if top:
1890 os.chdir(top)
1891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892 # Git patches have a/ at the beginning of source paths. We strip that out
1893 # with a sed script rather than the -p flag to patch so we can feed either
1894 # Git or svn-style patches into the same apply command.
1895 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001896 try:
1897 patch_data = subprocess2.check_output(
1898 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1899 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001900 DieWithError('Git patch mungling failed.')
1901 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001902 env = os.environ.copy()
1903 # 'cat' is a magical git string that disables pagers on all platforms.
1904 env['GIT_PAGER'] = 'cat'
1905
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906 # We use "git apply" to apply the patch instead of "patch" so that we can
1907 # pick up file adds.
1908 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001909 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 if options.reject:
1911 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001912 elif IsGitVersionAtLeast('1.7.12'):
1913 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001914 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001915 subprocess2.check_call(cmd, env=env,
1916 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001917 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 DieWithError('Failed to apply the patch')
1919
1920 # If we had an issue, commit the current state and register the issue.
1921 if not options.nocommit:
1922 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1923 cl = Changelist()
1924 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001925 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001926 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 else:
1928 print "Patch applied to index."
1929 return 0
1930
1931
1932def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001933 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934 # Provide a wrapper for git svn rebase to help avoid accidental
1935 # git svn dcommit.
1936 # It's the only command that doesn't use parser at all since we just defer
1937 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001938 env = os.environ.copy()
1939 # 'cat' is a magical git string that disables pagers on all platforms.
1940 env['GIT_PAGER'] = 'cat'
1941
1942 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943
1944
1945def GetTreeStatus():
1946 """Fetches the tree status and returns either 'open', 'closed',
1947 'unknown' or 'unset'."""
1948 url = settings.GetTreeStatusUrl(error_ok=True)
1949 if url:
1950 status = urllib2.urlopen(url).read().lower()
1951 if status.find('closed') != -1 or status == '0':
1952 return 'closed'
1953 elif status.find('open') != -1 or status == '1':
1954 return 'open'
1955 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001956 return 'unset'
1957
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001958
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959def GetTreeStatusReason():
1960 """Fetches the tree status from a json url and returns the message
1961 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001962 url = settings.GetTreeStatusUrl()
1963 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964 connection = urllib2.urlopen(json_url)
1965 status = json.loads(connection.read())
1966 connection.close()
1967 return status['message']
1968
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001971 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001972 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 status = GetTreeStatus()
1974 if 'unset' == status:
1975 print 'You must configure your tree status URL by running "git cl config".'
1976 return 2
1977
1978 print "The tree is %s" % status
1979 print
1980 print GetTreeStatusReason()
1981 if status != 'open':
1982 return 1
1983 return 0
1984
1985
maruel@chromium.org15192402012-09-06 12:38:29 +00001986def CMDtry(parser, args):
1987 """Triggers a try job through Rietveld."""
1988 group = optparse.OptionGroup(parser, "Try job options")
1989 group.add_option(
1990 "-b", "--bot", action="append",
1991 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1992 "times to specify multiple builders. ex: "
1993 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1994 "the try server waterfall for the builders name and the tests "
1995 "available. Can also be used to specify gtest_filter, e.g. "
1996 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1997 group.add_option(
1998 "-r", "--revision",
1999 help="Revision to use for the try job; default: the "
2000 "revision will be determined by the try server; see "
2001 "its waterfall for more info")
2002 group.add_option(
2003 "-c", "--clobber", action="store_true", default=False,
2004 help="Force a clobber before building; e.g. don't do an "
2005 "incremental build")
2006 group.add_option(
2007 "--project",
2008 help="Override which project to use. Projects are defined "
2009 "server-side to define what default bot set to use")
2010 group.add_option(
2011 "-t", "--testfilter", action="append", default=[],
2012 help=("Apply a testfilter to all the selected builders. Unless the "
2013 "builders configurations are similar, use multiple "
2014 "--bot <builder>:<test> arguments."))
2015 group.add_option(
2016 "-n", "--name", help="Try job name; default to current branch name")
2017 parser.add_option_group(group)
2018 options, args = parser.parse_args(args)
2019
2020 if args:
2021 parser.error('Unknown arguments: %s' % args)
2022
2023 cl = Changelist()
2024 if not cl.GetIssue():
2025 parser.error('Need to upload first')
2026
2027 if not options.name:
2028 options.name = cl.GetBranch()
2029
2030 # Process --bot and --testfilter.
2031 if not options.bot:
2032 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002033 change = cl.GetChange(
2034 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2035 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002036 options.bot = presubmit_support.DoGetTrySlaves(
2037 change,
2038 change.LocalPaths(),
2039 settings.GetRoot(),
2040 None,
2041 None,
2042 options.verbose,
2043 sys.stdout)
2044 if not options.bot:
2045 parser.error('No default try builder to try, use --bot')
2046
2047 builders_and_tests = {}
2048 for bot in options.bot:
2049 if ':' in bot:
2050 builder, tests = bot.split(':', 1)
2051 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2052 elif ',' in bot:
2053 parser.error('Specify one bot per --bot flag')
2054 else:
2055 builders_and_tests.setdefault(bot, []).append('defaulttests')
2056
2057 if options.testfilter:
2058 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2059 builders_and_tests = dict(
2060 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2061 if t != ['compile'])
2062
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002063 if any('triggered' in b for b in builders_and_tests):
2064 print >> sys.stderr, (
2065 'ERROR You are trying to send a job to a triggered bot. This type of'
2066 ' bot requires an\ninitial job from a parent (usually a builder). '
2067 'Instead send your job to the parent.\n'
2068 'Bot list: %s' % builders_and_tests)
2069 return 1
2070
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002071 patchset = cl.GetMostRecentPatchset()
2072 if patchset and patchset != cl.GetPatchset():
2073 print(
2074 '\nWARNING Mismatch between local config and server. Did a previous '
2075 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2076 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002077
2078 cl.RpcServer().trigger_try_jobs(
2079 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2080 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002081 print('Tried jobs on:')
2082 length = max(len(builder) for builder in builders_and_tests)
2083 for builder in sorted(builders_and_tests):
2084 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002085 return 0
2086
2087
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002088@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002090 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002091 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002092 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002093 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002094 return 0
2095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002097 if args:
2098 # One arg means set upstream branch.
2099 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2100 cl = Changelist()
2101 print "Upstream branch set to " + cl.GetUpstreamBranch()
2102 else:
2103 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104 return 0
2105
2106
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002107def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002108 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002109 _, args = parser.parse_args(args)
2110 if args:
2111 parser.error('Unrecognized args: %s' % ' '.join(args))
2112 cl = Changelist()
2113 cl.SetFlag('commit', '1')
2114 return 0
2115
2116
groby@chromium.org411034a2013-02-26 15:12:01 +00002117def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002118 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002119 _, args = parser.parse_args(args)
2120 if args:
2121 parser.error('Unrecognized args: %s' % ' '.join(args))
2122 cl = Changelist()
2123 # Ensure there actually is an issue to close.
2124 cl.GetDescription()
2125 cl.CloseIssue()
2126 return 0
2127
2128
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002129def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002130 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002131 CLANG_EXTS = ['.cc', '.cpp', '.h']
2132 parser.add_option('--full', action='store_true', default=False)
2133 opts, args = parser.parse_args(args)
2134 if args:
2135 parser.error('Unrecognized args: %s' % ' '.join(args))
2136
digit@chromium.org29e47272013-05-17 17:01:46 +00002137 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002138 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002139 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002140 # Only list the names of modified files.
2141 diff_cmd.append('--name-only')
2142 else:
2143 # Only generate context-less patches.
2144 diff_cmd.append('-U0')
2145
2146 # Grab the merge-base commit, i.e. the upstream commit of the current
2147 # branch when it was created or the last time it was rebased. This is
2148 # to cover the case where the user may have called "git fetch origin",
2149 # moving the origin branch to a newer commit, but hasn't rebased yet.
2150 upstream_commit = None
2151 cl = Changelist()
2152 upstream_branch = cl.GetUpstreamBranch()
2153 if upstream_branch:
2154 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2155 upstream_commit = upstream_commit.strip()
2156
2157 if not upstream_commit:
2158 DieWithError('Could not find base commit for this branch. '
2159 'Are you in detached state?')
2160
2161 diff_cmd.append(upstream_commit)
2162
2163 # Handle source file filtering.
2164 diff_cmd.append('--')
2165 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2166 diff_output = RunGit(diff_cmd)
2167
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002168 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2169
digit@chromium.org29e47272013-05-17 17:01:46 +00002170 if opts.full:
2171 # diff_output is a list of files to send to clang-format.
2172 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002173 if not files:
2174 print "Nothing to format."
2175 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002176 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2177 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002178 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002179 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002180 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2181 'clang-format-diff.py')
2182 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002183 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2184 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002185 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002186
2187 return 0
2188
2189
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002190class OptionParser(optparse.OptionParser):
2191 """Creates the option parse and add --verbose support."""
2192 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002193 optparse.OptionParser.__init__(
2194 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002195 self.add_option(
2196 '-v', '--verbose', action='count', default=0,
2197 help='Use 2 times for more debugging info')
2198
2199 def parse_args(self, args=None, values=None):
2200 options, args = optparse.OptionParser.parse_args(self, args, values)
2201 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2202 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2203 return options, args
2204
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002207 if sys.hexversion < 0x02060000:
2208 print >> sys.stderr, (
2209 '\nYour python version %s is unsupported, please upgrade.\n' %
2210 sys.version.split(' ', 1)[0])
2211 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002212
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002213 # Reload settings.
2214 global settings
2215 settings = Settings()
2216
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002217 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002218 dispatcher = subcommand.CommandDispatcher(__name__)
2219 try:
2220 return dispatcher.execute(OptionParser(), argv)
2221 except urllib2.HTTPError, e:
2222 if e.code != 500:
2223 raise
2224 DieWithError(
2225 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2226 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002227
2228
2229if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002230 # These affect sys.stdout so do it outside of main() to simplify mocks in
2231 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002232 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002233 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002234 sys.exit(main(sys.argv[1:]))