blob: 0ac0f861c70875fef8e888316e18051cd873f25b [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org967c0a82013-06-17 22:52:24 +000010import difflib
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024
25try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000026 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027except ImportError:
28 pass
29
maruel@chromium.org2a74d372011-03-29 19:05:50 +000030
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000031from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032from third_party import upload
33import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000034import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000035import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000037import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000039import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import watchlists
41
42
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000043DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000044POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000045DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000046GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000047CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000049# Shortcut since it quickly becomes redundant.
50Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000051
maruel@chromium.orgddd59412011-11-30 14:20:38 +000052# Initialized in main()
53settings = None
54
55
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000057 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 sys.exit(1)
59
60
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000063 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000064 except subprocess2.CalledProcessError as e:
65 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000068 'Command "%s" failed.\n%s' % (
69 ' '.join(args), error_message or e.stdout or ''))
70 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071
72
73def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000074 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000075 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
77
78def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000080 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000081 env = os.environ.copy()
82 # 'cat' is a magical git string that disables pagers on all platforms.
83 env['GIT_PAGER'] = 'cat'
84 out, code = subprocess2.communicate(['git'] + args,
85 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000086 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000087 return code, out[0]
88 except ValueError:
89 # When the subprocess fails, it returns None. That triggers a ValueError
90 # when trying to unpack the return value into (out, code).
91 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000092
93
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000094def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000095 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000096 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000097 return (version.startswith(prefix) and
98 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000099
100
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101def usage(more):
102 def hook(fn):
103 fn.usage_more = more
104 return fn
105 return hook
106
107
maruel@chromium.org90541732011-04-01 17:54:18 +0000108def ask_for_data(prompt):
109 try:
110 return raw_input(prompt)
111 except KeyboardInterrupt:
112 # Hide the exception.
113 sys.exit(1)
114
115
iannucci@chromium.org79540052012-10-19 23:15:26 +0000116def git_set_branch_value(key, value):
117 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000118 if not branch:
119 return
120
121 cmd = ['config']
122 if isinstance(value, int):
123 cmd.append('--int')
124 git_key = 'branch.%s.%s' % (branch, key)
125 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000126
127
128def git_get_branch_default(key, default):
129 branch = Changelist().GetBranch()
130 if branch:
131 git_key = 'branch.%s.%s' % (branch, key)
132 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
133 try:
134 return int(stdout.strip())
135 except ValueError:
136 pass
137 return default
138
139
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000140def add_git_similarity(parser):
141 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000143 help='Sets the percentage that a pair of files need to match in order to'
144 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000145 parser.add_option(
146 '--find-copies', action='store_true',
147 help='Allows git to look for copies.')
148 parser.add_option(
149 '--no-find-copies', action='store_false', dest='find_copies',
150 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000151
152 old_parser_args = parser.parse_args
153 def Parse(args):
154 options, args = old_parser_args(args)
155
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000158 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159 print('Note: Saving similarity of %d%% in git config.'
160 % options.similarity)
161 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000162
iannucci@chromium.org79540052012-10-19 23:15:26 +0000163 options.similarity = max(0, min(options.similarity, 100))
164
165 if options.find_copies is None:
166 options.find_copies = bool(
167 git_get_branch_default('git-find-copies', True))
168 else:
169 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000170
171 print('Using %d%% similarity for rename/copy detection. '
172 'Override with --similarity.' % options.similarity)
173
174 return options, args
175 parser.parse_args = Parse
176
177
ukai@chromium.org259e4682012-10-25 07:36:33 +0000178def is_dirty_git_tree(cmd):
179 # Make sure index is up-to-date before running diff-index.
180 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
181 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
182 if dirty:
183 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
184 print 'Uncommitted files: (git diff-index --name-status HEAD)'
185 print dirty[:4096]
186 if len(dirty) > 4096:
187 print '... (run "git diff-index --name-status HEAD" to see full output).'
188 return True
189 return False
190
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000191
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000192def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
193 """Return the corresponding git ref if |base_url| together with |glob_spec|
194 matches the full |url|.
195
196 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
197 """
198 fetch_suburl, as_ref = glob_spec.split(':')
199 if allow_wildcards:
200 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
201 if glob_match:
202 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
203 # "branches/{472,597,648}/src:refs/remotes/svn/*".
204 branch_re = re.escape(base_url)
205 if glob_match.group(1):
206 branch_re += '/' + re.escape(glob_match.group(1))
207 wildcard = glob_match.group(2)
208 if wildcard == '*':
209 branch_re += '([^/]*)'
210 else:
211 # Escape and replace surrounding braces with parentheses and commas
212 # with pipe symbols.
213 wildcard = re.escape(wildcard)
214 wildcard = re.sub('^\\\\{', '(', wildcard)
215 wildcard = re.sub('\\\\,', '|', wildcard)
216 wildcard = re.sub('\\\\}$', ')', wildcard)
217 branch_re += wildcard
218 if glob_match.group(3):
219 branch_re += re.escape(glob_match.group(3))
220 match = re.match(branch_re, url)
221 if match:
222 return re.sub('\*$', match.group(1), as_ref)
223
224 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
225 if fetch_suburl:
226 full_url = base_url + '/' + fetch_suburl
227 else:
228 full_url = base_url
229 if full_url == url:
230 return as_ref
231 return None
232
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000233
iannucci@chromium.org79540052012-10-19 23:15:26 +0000234def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000235 """Prints statistics about the change to the user."""
236 # --no-ext-diff is broken in some versions of Git, so try to work around
237 # this by overriding the environment (but there is still a problem if the
238 # git config key "diff.external" is used).
239 env = os.environ.copy()
240 if 'GIT_EXTERNAL_DIFF' in env:
241 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000242 # 'cat' is a magical git string that disables pagers on all platforms.
243 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000244
245 if find_copies:
246 similarity_options = ['--find-copies-harder', '-l100000',
247 '-C%s' % similarity]
248 else:
249 similarity_options = ['-M%s' % similarity]
250
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000251 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000252 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000253 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000254 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000255
256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000257class Settings(object):
258 def __init__(self):
259 self.default_server = None
260 self.cc = None
261 self.root = None
262 self.is_git_svn = None
263 self.svn_branch = None
264 self.tree_status_url = None
265 self.viewvc_url = None
266 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000267 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000268 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000269
270 def LazyUpdateIfNeeded(self):
271 """Updates the settings from a codereview.settings file, if available."""
272 if not self.updated:
273 cr_settings_file = FindCodereviewSettingsFile()
274 if cr_settings_file:
275 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000276 self.updated = True
277 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278 self.updated = True
279
280 def GetDefaultServerUrl(self, error_ok=False):
281 if not self.default_server:
282 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000283 self.default_server = gclient_utils.UpgradeToHttps(
284 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285 if error_ok:
286 return self.default_server
287 if not self.default_server:
288 error_message = ('Could not find settings file. You must configure '
289 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000290 self.default_server = gclient_utils.UpgradeToHttps(
291 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292 return self.default_server
293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000294 def GetRoot(self):
295 if not self.root:
296 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
297 return self.root
298
299 def GetIsGitSvn(self):
300 """Return true if this repo looks like it's using git-svn."""
301 if self.is_git_svn is None:
302 # If you have any "svn-remote.*" config keys, we think you're using svn.
303 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000304 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000305 return self.is_git_svn
306
307 def GetSVNBranch(self):
308 if self.svn_branch is None:
309 if not self.GetIsGitSvn():
310 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
311
312 # Try to figure out which remote branch we're based on.
313 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000314 # 1) iterate through our branch history and find the svn URL.
315 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316
317 # regexp matching the git-svn line that contains the URL.
318 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
319
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000320 env = os.environ.copy()
321 # 'cat' is a magical git string that disables pagers on all platforms.
322 env['GIT_PAGER'] = 'cat'
323
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000324 # We don't want to go through all of history, so read a line from the
325 # pipe at a time.
326 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000327 cmd = ['git', 'log', '-100', '--pretty=medium']
328 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000329 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000330 for line in proc.stdout:
331 match = git_svn_re.match(line)
332 if match:
333 url = match.group(1)
334 proc.stdout.close() # Cut pipe.
335 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000336
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000337 if url:
338 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
339 remotes = RunGit(['config', '--get-regexp',
340 r'^svn-remote\..*\.url']).splitlines()
341 for remote in remotes:
342 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000343 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000344 remote = match.group(1)
345 base_url = match.group(2)
346 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000347 ['config', 'svn-remote.%s.fetch' % remote],
348 error_ok=True).strip()
349 if fetch_spec:
350 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
351 if self.svn_branch:
352 break
353 branch_spec = RunGit(
354 ['config', 'svn-remote.%s.branches' % remote],
355 error_ok=True).strip()
356 if branch_spec:
357 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
358 if self.svn_branch:
359 break
360 tag_spec = RunGit(
361 ['config', 'svn-remote.%s.tags' % remote],
362 error_ok=True).strip()
363 if tag_spec:
364 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
365 if self.svn_branch:
366 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000367
368 if not self.svn_branch:
369 DieWithError('Can\'t guess svn branch -- try specifying it on the '
370 'command line')
371
372 return self.svn_branch
373
374 def GetTreeStatusUrl(self, error_ok=False):
375 if not self.tree_status_url:
376 error_message = ('You must configure your tree status URL by running '
377 '"git cl config".')
378 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
379 error_ok=error_ok,
380 error_message=error_message)
381 return self.tree_status_url
382
383 def GetViewVCUrl(self):
384 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000385 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000386 return self.viewvc_url
387
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000388 def GetDefaultCCList(self):
389 return self._GetConfig('rietveld.cc', error_ok=True)
390
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000391 def GetDefaultPrivateFlag(self):
392 return self._GetConfig('rietveld.private', error_ok=True)
393
ukai@chromium.orge8077812012-02-03 03:41:46 +0000394 def GetIsGerrit(self):
395 """Return true if this repo is assosiated with gerrit code review system."""
396 if self.is_gerrit is None:
397 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
398 return self.is_gerrit
399
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000400 def GetGitEditor(self):
401 """Return the editor specified in the git config, or None if none is."""
402 if self.git_editor is None:
403 self.git_editor = self._GetConfig('core.editor', error_ok=True)
404 return self.git_editor or None
405
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000406 def _GetConfig(self, param, **kwargs):
407 self.LazyUpdateIfNeeded()
408 return RunGit(['config', param], **kwargs).strip()
409
410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000411def ShortBranchName(branch):
412 """Convert a name like 'refs/heads/foo' to just 'foo'."""
413 return branch.replace('refs/heads/', '')
414
415
416class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000417 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000418 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000419 global settings
420 if not settings:
421 # Happens when git_cl.py is used as a utility library.
422 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000423 settings.GetDefaultServerUrl()
424 self.branchref = branchref
425 if self.branchref:
426 self.branch = ShortBranchName(self.branchref)
427 else:
428 self.branch = None
429 self.rietveld_server = None
430 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000431 self.lookedup_issue = False
432 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433 self.has_description = False
434 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000435 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000437 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000438 self.cc = None
439 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000440 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000441 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000442
443 def GetCCList(self):
444 """Return the users cc'd on this CL.
445
446 Return is a string suitable for passing to gcl with the --cc flag.
447 """
448 if self.cc is None:
449 base_cc = settings .GetDefaultCCList()
450 more_cc = ','.join(self.watchers)
451 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
452 return self.cc
453
454 def SetWatchers(self, watchers):
455 """Set the list of email addresses that should be cc'd based on the changed
456 files in this CL.
457 """
458 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459
460 def GetBranch(self):
461 """Returns the short branch name, e.g. 'master'."""
462 if not self.branch:
463 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
464 self.branch = ShortBranchName(self.branchref)
465 return self.branch
466
467 def GetBranchRef(self):
468 """Returns the full branch name, e.g. 'refs/heads/master'."""
469 self.GetBranch() # Poke the lazy loader.
470 return self.branchref
471
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000472 @staticmethod
473 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000474 """Returns a tuple containg remote and remote ref,
475 e.g. 'origin', 'refs/heads/master'
476 """
477 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
479 error_ok=True).strip()
480 if upstream_branch:
481 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
482 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000483 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
484 error_ok=True).strip()
485 if upstream_branch:
486 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000487 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000488 # Fall back on trying a git-svn upstream branch.
489 if settings.GetIsGitSvn():
490 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000492 # Else, try to guess the origin remote.
493 remote_branches = RunGit(['branch', '-r']).split()
494 if 'origin/master' in remote_branches:
495 # Fall back on origin/master if it exits.
496 remote = 'origin'
497 upstream_branch = 'refs/heads/master'
498 elif 'origin/trunk' in remote_branches:
499 # Fall back on origin/trunk if it exists. Generally a shared
500 # git-svn clone
501 remote = 'origin'
502 upstream_branch = 'refs/heads/trunk'
503 else:
504 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000505Either pass complete "git diff"-style arguments, like
506 git cl upload origin/master
507or verify this branch is set up to track another (via the --track argument to
508"git checkout -b ...").""")
509
510 return remote, upstream_branch
511
512 def GetUpstreamBranch(self):
513 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000514 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000515 if remote is not '.':
516 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
517 self.upstream_branch = upstream_branch
518 return self.upstream_branch
519
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000520 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000521 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000522 remote, branch = None, self.GetBranch()
523 seen_branches = set()
524 while branch not in seen_branches:
525 seen_branches.add(branch)
526 remote, branch = self.FetchUpstreamTuple(branch)
527 branch = ShortBranchName(branch)
528 if remote != '.' or branch.startswith('refs/remotes'):
529 break
530 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000531 remotes = RunGit(['remote'], error_ok=True).split()
532 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000533 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000534 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000535 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000536 logging.warning('Could not determine which remote this change is '
537 'associated with, so defaulting to "%s". This may '
538 'not be what you want. You may prevent this message '
539 'by running "git svn info" as documented here: %s',
540 self._remote,
541 GIT_INSTRUCTIONS_URL)
542 else:
543 logging.warn('Could not determine which remote this change is '
544 'associated with. You may prevent this message by '
545 'running "git svn info" as documented here: %s',
546 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000547 branch = 'HEAD'
548 if branch.startswith('refs/remotes'):
549 self._remote = (remote, branch)
550 else:
551 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000552 return self._remote
553
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000554 def GitSanityChecks(self, upstream_git_obj):
555 """Checks git repo status and ensures diff is from local commits."""
556
557 # Verify the commit we're diffing against is in our current branch.
558 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
559 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
560 if upstream_sha != common_ancestor:
561 print >> sys.stderr, (
562 'ERROR: %s is not in the current branch. You may need to rebase '
563 'your tracking branch' % upstream_sha)
564 return False
565
566 # List the commits inside the diff, and verify they are all local.
567 commits_in_diff = RunGit(
568 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
569 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
570 remote_branch = remote_branch.strip()
571 if code != 0:
572 _, remote_branch = self.GetRemoteBranch()
573
574 commits_in_remote = RunGit(
575 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
576
577 common_commits = set(commits_in_diff) & set(commits_in_remote)
578 if common_commits:
579 print >> sys.stderr, (
580 'ERROR: Your diff contains %d commits already in %s.\n'
581 'Run "git log --oneline %s..HEAD" to get a list of commits in '
582 'the diff. If you are using a custom git flow, you can override'
583 ' the reference used for this check with "git config '
584 'gitcl.remotebranch <git-ref>".' % (
585 len(common_commits), remote_branch, upstream_git_obj))
586 return False
587 return True
588
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000589 def GetGitBaseUrlFromConfig(self):
590 """Return the configured base URL from branch.<branchname>.baseurl.
591
592 Returns None if it is not set.
593 """
594 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
595 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000596
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597 def GetRemoteUrl(self):
598 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
599
600 Returns None if there is no remote.
601 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000602 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
604
605 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000606 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000607 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000609 self.issue = int(issue) or None if issue else None
610 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 return self.issue
612
613 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000614 if not self.rietveld_server:
615 # If we're on a branch then get the server potentially associated
616 # with that branch.
617 if self.GetIssue():
618 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
619 ['config', self._RietveldServer()], error_ok=True).strip())
620 if not self.rietveld_server:
621 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.rietveld_server
623
624 def GetIssueURL(self):
625 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000626 if not self.GetIssue():
627 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
629
630 def GetDescription(self, pretty=False):
631 if not self.has_description:
632 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000633 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000634 try:
635 self.description = self.RpcServer().get_description(issue).strip()
636 except urllib2.HTTPError, e:
637 if e.code == 404:
638 DieWithError(
639 ('\nWhile fetching the description for issue %d, received a '
640 '404 (not found)\n'
641 'error. It is likely that you deleted this '
642 'issue on the server. If this is the\n'
643 'case, please run\n\n'
644 ' git cl issue 0\n\n'
645 'to clear the association with the deleted issue. Then run '
646 'this command again.') % issue)
647 else:
648 DieWithError(
649 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 self.has_description = True
651 if pretty:
652 wrapper = textwrap.TextWrapper()
653 wrapper.initial_indent = wrapper.subsequent_indent = ' '
654 return wrapper.fill(self.description)
655 return self.description
656
657 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000658 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000659 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 patchset = RunGit(['config', self._PatchsetSetting()],
661 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000662 self.patchset = int(patchset) or None if patchset else None
663 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000664 return self.patchset
665
666 def SetPatchset(self, patchset):
667 """Set this branch's patchset. If patchset=0, clears the patchset."""
668 if patchset:
669 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000670 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671 else:
672 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000673 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000674 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000676 def GetMostRecentPatchset(self):
677 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000678
679 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000680 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000681 '/download/issue%s_%s.diff' % (issue, patchset))
682
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000683 def GetIssueProperties(self):
684 if self._props is None:
685 issue = self.GetIssue()
686 if not issue:
687 self._props = {}
688 else:
689 self._props = self.RpcServer().get_issue_properties(issue, True)
690 return self._props
691
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000692 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000693 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000694
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 def SetIssue(self, issue):
696 """Set this branch's issue. If issue=0, clears the issue."""
697 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000698 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000699 RunGit(['config', self._IssueSetting(), str(issue)])
700 if self.rietveld_server:
701 RunGit(['config', self._RietveldServer(), self.rietveld_server])
702 else:
703 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000704 self.issue = None
705 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000707 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000708 if not self.GitSanityChecks(upstream_branch):
709 DieWithError('\nGit sanity check failure')
710
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000711 env = os.environ.copy()
712 # 'cat' is a magical git string that disables pagers on all platforms.
713 env['GIT_PAGER'] = 'cat'
714
715 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000716 if not root:
717 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000718 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000719
720 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000721 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000722 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000723 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000724 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000725 except subprocess2.CalledProcessError:
726 DieWithError(
727 ('\nFailed to diff against upstream branch %s!\n\n'
728 'This branch probably doesn\'t exist anymore. To reset the\n'
729 'tracking branch, please run\n'
730 ' git branch --set-upstream %s trunk\n'
731 'replacing trunk with origin/master or the relevant branch') %
732 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000733
maruel@chromium.org52424302012-08-29 15:14:30 +0000734 issue = self.GetIssue()
735 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000736 if issue:
737 description = self.GetDescription()
738 else:
739 # If the change was never uploaded, use the log messages of all commits
740 # up to the branch point, as git cl upload will prefill the description
741 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000742 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000743 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000744 '%s...' % (upstream_branch)],
745 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000746
747 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000748 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000749 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000750 name,
751 description,
752 absroot,
753 files,
754 issue,
755 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000756 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000757
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000758 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000759 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000760
761 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000762 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000763 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000764 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000765 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000766 except presubmit_support.PresubmitFailure, e:
767 DieWithError(
768 ('%s\nMaybe your depot_tools is out of date?\n'
769 'If all fails, contact maruel@') % e)
770
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000771 def UpdateDescription(self, description):
772 self.description = description
773 return self.RpcServer().update_description(
774 self.GetIssue(), self.description)
775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000777 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000778 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000780 def SetFlag(self, flag, value):
781 """Patchset must match."""
782 if not self.GetPatchset():
783 DieWithError('The patchset needs to match. Send another patchset.')
784 try:
785 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000786 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000787 except urllib2.HTTPError, e:
788 if e.code == 404:
789 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
790 if e.code == 403:
791 DieWithError(
792 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
793 'match?') % (self.GetIssue(), self.GetPatchset()))
794 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000796 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 """Returns an upload.RpcServer() to access this review's rietveld instance.
798 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000799 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000800 self._rpc_server = rietveld.CachingRietveld(
801 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000802 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803
804 def _IssueSetting(self):
805 """Return the git setting that stores this change's issue."""
806 return 'branch.%s.rietveldissue' % self.GetBranch()
807
808 def _PatchsetSetting(self):
809 """Return the git setting that stores this change's most recent patchset."""
810 return 'branch.%s.rietveldpatchset' % self.GetBranch()
811
812 def _RietveldServer(self):
813 """Returns the git setting that stores this change's rietveld server."""
814 return 'branch.%s.rietveldserver' % self.GetBranch()
815
816
817def GetCodereviewSettingsInteractively():
818 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000819 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000820 server = settings.GetDefaultServerUrl(error_ok=True)
821 prompt = 'Rietveld server (host[:port])'
822 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000823 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 if not server and not newserver:
825 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000826 if newserver:
827 newserver = gclient_utils.UpgradeToHttps(newserver)
828 if newserver != server:
829 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000831 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 prompt = caption
833 if initial:
834 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000835 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 if new_val == 'x':
837 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000838 elif new_val:
839 if is_url:
840 new_val = gclient_utils.UpgradeToHttps(new_val)
841 if new_val != initial:
842 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000844 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000845 SetProperty(settings.GetDefaultPrivateFlag(),
846 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000848 'tree-status-url', False)
849 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850
851 # TODO: configure a default branch to diff against, rather than this
852 # svn-based hackery.
853
854
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000855class ChangeDescription(object):
856 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000857 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000858
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000859 def __init__(self, description):
860 self._description = (description or '').strip()
861
862 @property
863 def description(self):
864 return self._description
865
866 def update_reviewers(self, reviewers):
867 """Rewrites the R=/TBR= line(s) as a single line."""
868 assert isinstance(reviewers, list), reviewers
869 if not reviewers:
870 return
871 regexp = re.compile(self.R_LINE, re.MULTILINE)
872 matches = list(regexp.finditer(self._description))
873 is_tbr = any(m.group(1) == 'TBR' for m in matches)
874 if len(matches) > 1:
875 # Erase all except the first one.
876 for i in xrange(len(matches) - 1, 0, -1):
877 self._description = (
878 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000879 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880
881 if is_tbr:
882 new_r_line = 'TBR=' + ', '.join(reviewers)
883 else:
884 new_r_line = 'R=' + ', '.join(reviewers)
885
886 if matches:
887 self._description = (
888 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000889 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000890 else:
891 self.append_footer(new_r_line)
892
893 def prompt(self):
894 """Asks the user to update the description."""
895 self._description = (
896 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000897 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000898 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000899 '#--------------------This line is 72 characters long'
900 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000901 ) + self._description
902
903 if '\nBUG=' not in self._description:
904 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000905 content = gclient_utils.RunEditor(self._description, True,
906 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000907 if not content:
908 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000909
910 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000911 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000912 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000913 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000914 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000915
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000916 def append_footer(self, line):
917 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
918 if self._description:
919 if '\n' not in self._description:
920 self._description += '\n'
921 else:
922 last_line = self._description.rsplit('\n', 1)[1]
923 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
924 not presubmit_support.Change.TAG_LINE_RE.match(line)):
925 self._description += '\n'
926 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000927
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000928 def get_reviewers(self):
929 """Retrieves the list of reviewers."""
930 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000931 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000932 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000933
934
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000935def get_approving_reviewers(props):
936 """Retrieves the reviewers that approved a CL from the issue properties with
937 messages.
938
939 Note that the list may contain reviewers that are not committer, thus are not
940 considered by the CQ.
941 """
942 return sorted(
943 set(
944 message['sender']
945 for message in props['messages']
946 if message['approval'] and message['sender'] in props['reviewers']
947 )
948 )
949
950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951def FindCodereviewSettingsFile(filename='codereview.settings'):
952 """Finds the given file starting in the cwd and going up.
953
954 Only looks up to the top of the repository unless an
955 'inherit-review-settings-ok' file exists in the root of the repository.
956 """
957 inherit_ok_file = 'inherit-review-settings-ok'
958 cwd = os.getcwd()
959 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
960 if os.path.isfile(os.path.join(root, inherit_ok_file)):
961 root = '/'
962 while True:
963 if filename in os.listdir(cwd):
964 if os.path.isfile(os.path.join(cwd, filename)):
965 return open(os.path.join(cwd, filename))
966 if cwd == root:
967 break
968 cwd = os.path.dirname(cwd)
969
970
971def LoadCodereviewSettingsFromFile(fileobj):
972 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000973 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975 def SetProperty(name, setting, unset_error_ok=False):
976 fullname = 'rietveld.' + name
977 if setting in keyvals:
978 RunGit(['config', fullname, keyvals[setting]])
979 else:
980 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
981
982 SetProperty('server', 'CODE_REVIEW_SERVER')
983 # Only server setting is required. Other settings can be absent.
984 # In that case, we ignore errors raised during option deletion attempt.
985 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000986 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
988 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
989
ukai@chromium.orge8077812012-02-03 03:41:46 +0000990 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
991 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
992 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000994 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
995 #should be of the form
996 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
997 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
998 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
999 keyvals['ORIGIN_URL_CONFIG']])
1000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001002def urlretrieve(source, destination):
1003 """urllib is broken for SSL connections via a proxy therefore we
1004 can't use urllib.urlretrieve()."""
1005 with open(destination, 'w') as f:
1006 f.write(urllib2.urlopen(source).read())
1007
1008
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001009def DownloadHooks(force):
1010 """downloads hooks
1011
1012 Args:
1013 force: True to update hooks. False to install hooks if not present.
1014 """
1015 if not settings.GetIsGerrit():
1016 return
1017 server_url = settings.GetDefaultServerUrl()
1018 src = '%s/tools/hooks/commit-msg' % server_url
1019 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1020 if not os.access(dst, os.X_OK):
1021 if os.path.exists(dst):
1022 if not force:
1023 return
1024 os.remove(dst)
1025 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001026 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001027 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1028 except Exception:
1029 if os.path.exists(dst):
1030 os.remove(dst)
1031 DieWithError('\nFailed to download hooks from %s' % src)
1032
1033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034@usage('[repo root containing codereview.settings]')
1035def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001036 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001038 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 if len(args) == 0:
1040 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001041 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 return 0
1043
1044 url = args[0]
1045 if not url.endswith('codereview.settings'):
1046 url = os.path.join(url, 'codereview.settings')
1047
1048 # Load code review settings and download hooks (if available).
1049 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001050 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 return 0
1052
1053
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001054def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001055 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001056 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1057 branch = ShortBranchName(branchref)
1058 _, args = parser.parse_args(args)
1059 if not args:
1060 print("Current base-url:")
1061 return RunGit(['config', 'branch.%s.base-url' % branch],
1062 error_ok=False).strip()
1063 else:
1064 print("Setting base-url to %s" % args[0])
1065 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1066 error_ok=False).strip()
1067
1068
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001070 """Show status of changelists.
1071
1072 Colors are used to tell the state of the CL unless --fast is used:
1073 - Green LGTM'ed
1074 - Blue waiting for review
1075 - Yellow waiting for you to reply to review
1076 - Red not sent for review or broken
1077 - Cyan was committed, branch can be deleted
1078
1079 Also see 'git cl comments'.
1080 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 parser.add_option('--field',
1082 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001083 parser.add_option('-f', '--fast', action='store_true',
1084 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 (options, args) = parser.parse_args(args)
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001088 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089 if options.field.startswith('desc'):
1090 print cl.GetDescription()
1091 elif options.field == 'id':
1092 issueid = cl.GetIssue()
1093 if issueid:
1094 print issueid
1095 elif options.field == 'patch':
1096 patchset = cl.GetPatchset()
1097 if patchset:
1098 print patchset
1099 elif options.field == 'url':
1100 url = cl.GetIssueURL()
1101 if url:
1102 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001103 return 0
1104
1105 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1106 if not branches:
1107 print('No local branch found.')
1108 return 0
1109
1110 changes = (Changelist(branchref=b) for b in branches.splitlines())
1111 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1112 alignment = max(5, max(len(b) for b in branches))
1113 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001114 # Adhoc thread pool to request data concurrently.
1115 output = Queue.Queue()
1116
1117 # Silence upload.py otherwise it becomes unweldly.
1118 upload.verbosity = 0
1119
1120 if not options.fast:
1121 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001122 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001123 c = Changelist(branchref=b)
1124 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001125 props = {}
1126 r = None
1127 if i:
1128 try:
1129 props = c.GetIssueProperties()
1130 r = c.GetApprovingReviewers() if i else None
1131 except urllib2.HTTPError:
1132 # The issue probably doesn't exist anymore.
1133 i += ' (broken)'
1134
1135 msgs = props.get('messages') or []
1136
1137 if not i:
1138 color = Fore.WHITE
1139 elif props.get('closed'):
1140 # Issue is closed.
1141 color = Fore.CYAN
1142 elif r:
1143 # Was LGTM'ed.
1144 color = Fore.GREEN
1145 elif not msgs:
1146 # No message was sent.
1147 color = Fore.RED
1148 elif msgs[-1]['sender'] != props.get('owner_email'):
1149 color = Fore.YELLOW
1150 else:
1151 color = Fore.BLUE
1152 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001153
1154 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1155 for t in threads:
1156 t.daemon = True
1157 t.start()
1158 else:
1159 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1160 for b in branches:
1161 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001162 url = c.GetIssueURL()
1163 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001164
1165 tmp = {}
1166 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001167 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001168 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001169 b, i, color = output.get()
1170 tmp[b] = (i, color)
1171 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001172 reset = Fore.RESET
1173 if not sys.stdout.isatty():
1174 color = ''
1175 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001176 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001177 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001178
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001179 cl = Changelist()
1180 print
1181 print 'Current branch:',
1182 if not cl.GetIssue():
1183 print 'no issue assigned.'
1184 return 0
1185 print cl.GetBranch()
1186 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1187 print 'Issue description:'
1188 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 return 0
1190
1191
1192@usage('[issue_number]')
1193def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001194 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195
1196 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001197 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001198 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199
1200 cl = Changelist()
1201 if len(args) > 0:
1202 try:
1203 issue = int(args[0])
1204 except ValueError:
1205 DieWithError('Pass a number to set the issue or none to list it.\n'
1206 'Maybe you want to run git cl status?')
1207 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001208 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 return 0
1210
1211
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001212def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001213 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001214 (_, args) = parser.parse_args(args)
1215 if args:
1216 parser.error('Unsupported argument: %s' % args)
1217
1218 cl = Changelist()
1219 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001220 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001221 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001222 if message['disapproval']:
1223 color = Fore.RED
1224 elif message['approval']:
1225 color = Fore.GREEN
1226 elif message['sender'] == data['owner_email']:
1227 color = Fore.MAGENTA
1228 else:
1229 color = Fore.BLUE
1230 print '\n%s%s %s%s' % (
1231 color, message['date'].split('.', 1)[0], message['sender'],
1232 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001233 if message['text'].strip():
1234 print '\n'.join(' ' + l for l in message['text'].splitlines())
1235 return 0
1236
1237
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001238def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001239 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001240 cl = Changelist()
1241 if not cl.GetIssue():
1242 DieWithError('This branch has no associated changelist.')
1243 description = ChangeDescription(cl.GetDescription())
1244 description.prompt()
1245 cl.UpdateDescription(description.description)
1246 return 0
1247
1248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249def CreateDescriptionFromLog(args):
1250 """Pulls out the commit log to use as a base for the CL description."""
1251 log_args = []
1252 if len(args) == 1 and not args[0].endswith('.'):
1253 log_args = [args[0] + '..']
1254 elif len(args) == 1 and args[0].endswith('...'):
1255 log_args = [args[0][:-1]]
1256 elif len(args) == 2:
1257 log_args = [args[0] + '..' + args[1]]
1258 else:
1259 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001260 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261
1262
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001264 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001265 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001267 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001268 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 (options, args) = parser.parse_args(args)
1270
ukai@chromium.org259e4682012-10-25 07:36:33 +00001271 if not options.force and is_dirty_git_tree('presubmit'):
1272 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 return 1
1274
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001275 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 if args:
1277 base_branch = args[0]
1278 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 # Default to diffing against the common ancestor of the upstream branch.
1280 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001282 cl.RunHook(
1283 committing=not options.upload,
1284 may_prompt=False,
1285 verbose=options.verbose,
1286 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001287 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288
1289
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001290def AddChangeIdToCommitMessage(options, args):
1291 """Re-commits using the current message, assumes the commit hook is in
1292 place.
1293 """
1294 log_desc = options.message or CreateDescriptionFromLog(args)
1295 git_command = ['commit', '--amend', '-m', log_desc]
1296 RunGit(git_command)
1297 new_log_desc = CreateDescriptionFromLog(args)
1298 if CHANGE_ID in new_log_desc:
1299 print 'git-cl: Added Change-Id to commit message.'
1300 else:
1301 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1302
1303
ukai@chromium.orge8077812012-02-03 03:41:46 +00001304def GerritUpload(options, args, cl):
1305 """upload the current branch to gerrit."""
1306 # We assume the remote called "origin" is the one we want.
1307 # It is probably not worthwhile to support different workflows.
1308 remote = 'origin'
1309 branch = 'master'
1310 if options.target_branch:
1311 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001313 change_desc = ChangeDescription(
1314 options.message or CreateDescriptionFromLog(args))
1315 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001316 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001318 if CHANGE_ID not in change_desc.description:
1319 AddChangeIdToCommitMessage(options, args)
1320 if options.reviewers:
1321 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
ukai@chromium.orge8077812012-02-03 03:41:46 +00001323 receive_options = []
1324 cc = cl.GetCCList().split(',')
1325 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001326 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001327 cc = filter(None, cc)
1328 if cc:
1329 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001330 if change_desc.get_reviewers():
1331 receive_options.extend(
1332 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333
ukai@chromium.orge8077812012-02-03 03:41:46 +00001334 git_command = ['push']
1335 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001336 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001337 ' '.join(receive_options))
1338 git_command += [remote, 'HEAD:refs/for/' + branch]
1339 RunGit(git_command)
1340 # TODO(ukai): parse Change-Id: and set issue number?
1341 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001342
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343
ukai@chromium.orge8077812012-02-03 03:41:46 +00001344def RietveldUpload(options, args, cl):
1345 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1347 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348 if options.emulate_svn_auto_props:
1349 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
1351 change_desc = None
1352
1353 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001354 if options.title:
1355 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001356 if options.message:
1357 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001358 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 print ("This branch is associated with issue %s. "
1360 "Adding patch to that issue." % cl.GetIssue())
1361 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001362 if options.title:
1363 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001364 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001365 change_desc = ChangeDescription(message)
1366 if options.reviewers:
1367 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001368 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001369 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001370
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001371 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 print "Description is empty; aborting."
1373 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001374
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001375 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001376 if change_desc.get_reviewers():
1377 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001378 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001379 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001380 DieWithError("Must specify reviewers to send email.")
1381 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001382 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001383 if cc:
1384 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001386 if options.private or settings.GetDefaultPrivateFlag() == "True":
1387 upload_args.append('--private')
1388
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001389 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001390 if not options.find_copies:
1391 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393 # Include the upstream repo's URL in the change -- this is useful for
1394 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001395 remote_url = cl.GetGitBaseUrlFromConfig()
1396 if not remote_url:
1397 if settings.GetIsGitSvn():
1398 # URL is dependent on the current directory.
1399 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1400 if data:
1401 keys = dict(line.split(': ', 1) for line in data.splitlines()
1402 if ': ' in line)
1403 remote_url = keys.get('URL', None)
1404 else:
1405 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1406 remote_url = (cl.GetRemoteUrl() + '@'
1407 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 if remote_url:
1409 upload_args.extend(['--base_url', remote_url])
1410
1411 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001412 upload_args = ['upload'] + upload_args + args
1413 logging.info('upload.RealMain(%s)', upload_args)
1414 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001415 except KeyboardInterrupt:
1416 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 except:
1418 # If we got an exception after the user typed a description for their
1419 # change, back up the description before re-raising.
1420 if change_desc:
1421 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1422 print '\nGot exception while uploading -- saving description to %s\n' \
1423 % backup_path
1424 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001425 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 backup_file.close()
1427 raise
1428
1429 if not cl.GetIssue():
1430 cl.SetIssue(issue)
1431 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001432
1433 if options.use_commit_queue:
1434 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 return 0
1436
1437
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001438def cleanup_list(l):
1439 """Fixes a list so that comma separated items are put as individual items.
1440
1441 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1442 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1443 """
1444 items = sum((i.split(',') for i in l), [])
1445 stripped_items = (i.strip() for i in items)
1446 return sorted(filter(None, stripped_items))
1447
1448
ukai@chromium.orge8077812012-02-03 03:41:46 +00001449@usage('[args to "git diff"]')
1450def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001451 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001452 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1453 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001454 parser.add_option('--bypass-watchlists', action='store_true',
1455 dest='bypass_watchlists',
1456 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001457 parser.add_option('-f', action='store_true', dest='force',
1458 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001459 parser.add_option('-m', dest='message', help='message for patchset')
1460 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001461 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001462 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001463 help='reviewer email addresses')
1464 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001465 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001466 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001467 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001468 help='send email to reviewer immediately')
1469 parser.add_option("--emulate_svn_auto_props", action="store_true",
1470 dest="emulate_svn_auto_props",
1471 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001472 parser.add_option('-c', '--use-commit-queue', action='store_true',
1473 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001474 parser.add_option('--private', action='store_true',
1475 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001476 parser.add_option('--target_branch',
1477 help='When uploading to gerrit, remote branch to '
1478 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001479 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001480 (options, args) = parser.parse_args(args)
1481
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001482 if options.target_branch and not settings.GetIsGerrit():
1483 parser.error('Use --target_branch for non gerrit repository.')
1484
ukai@chromium.org259e4682012-10-25 07:36:33 +00001485 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001486 return 1
1487
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001488 options.reviewers = cleanup_list(options.reviewers)
1489 options.cc = cleanup_list(options.cc)
1490
ukai@chromium.orge8077812012-02-03 03:41:46 +00001491 cl = Changelist()
1492 if args:
1493 # TODO(ukai): is it ok for gerrit case?
1494 base_branch = args[0]
1495 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001496 # Default to diffing against common ancestor of upstream branch
1497 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001498 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001499
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001500 # Apply watchlists on upload.
1501 change = cl.GetChange(base_branch, None)
1502 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1503 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001504 if not options.bypass_watchlists:
1505 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001506
ukai@chromium.orge8077812012-02-03 03:41:46 +00001507 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001508 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001509 may_prompt=not options.force,
1510 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001511 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001512 if not hook_results.should_continue():
1513 return 1
1514 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001515 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001516
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001517 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001518 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001519 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001520 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001521 print ('The last upload made from this repository was patchset #%d but '
1522 'the most recent patchset on the server is #%d.'
1523 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001524 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1525 'from another machine or branch the patch you\'re uploading now '
1526 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001527 ask_for_data('About to upload; enter to confirm.')
1528
iannucci@chromium.org79540052012-10-19 23:15:26 +00001529 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001530 if settings.GetIsGerrit():
1531 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001532 ret = RietveldUpload(options, args, cl)
1533 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001534 git_set_branch_value('last-upload-hash',
1535 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001536
1537 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001538
1539
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001540def IsSubmoduleMergeCommit(ref):
1541 # When submodules are added to the repo, we expect there to be a single
1542 # non-git-svn merge commit at remote HEAD with a signature comment.
1543 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001544 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001545 return RunGit(cmd) != ''
1546
1547
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548def SendUpstream(parser, args, cmd):
1549 """Common code for CmdPush and CmdDCommit
1550
1551 Squashed commit into a single.
1552 Updates changelog with metadata (e.g. pointer to review).
1553 Pushes/dcommits the code upstream.
1554 Updates review and closes.
1555 """
1556 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1557 help='bypass upload presubmit hook')
1558 parser.add_option('-m', dest='message',
1559 help="override review description")
1560 parser.add_option('-f', action='store_true', dest='force',
1561 help="force yes to questions (don't prompt)")
1562 parser.add_option('-c', dest='contributor',
1563 help="external contributor for patch (appended to " +
1564 "description and used as author for git). Should be " +
1565 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001566 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567 (options, args) = parser.parse_args(args)
1568 cl = Changelist()
1569
1570 if not args or cmd == 'push':
1571 # Default to merging against our best guess of the upstream branch.
1572 args = [cl.GetUpstreamBranch()]
1573
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001574 if options.contributor:
1575 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1576 print "Please provide contibutor as 'First Last <email@example.com>'"
1577 return 1
1578
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001580 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001581
ukai@chromium.org259e4682012-10-25 07:36:33 +00001582 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583 return 1
1584
1585 # This rev-list syntax means "show all commits not in my branch that
1586 # are in base_branch".
1587 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1588 base_branch]).splitlines()
1589 if upstream_commits:
1590 print ('Base branch "%s" has %d commits '
1591 'not in this branch.' % (base_branch, len(upstream_commits)))
1592 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1593 return 1
1594
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001595 # This is the revision `svn dcommit` will commit on top of.
1596 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1597 '--pretty=format:%H'])
1598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001599 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001600 # If the base_head is a submodule merge commit, the first parent of the
1601 # base_head should be a git-svn commit, which is what we're interested in.
1602 base_svn_head = base_branch
1603 if base_has_submodules:
1604 base_svn_head += '^1'
1605
1606 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607 if extra_commits:
1608 print ('This branch has %d additional commits not upstreamed yet.'
1609 % len(extra_commits.splitlines()))
1610 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1611 'before attempting to %s.' % (base_branch, cmd))
1612 return 1
1613
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001614 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001615 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001616 author = None
1617 if options.contributor:
1618 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001619 hook_results = cl.RunHook(
1620 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001621 may_prompt=not options.force,
1622 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001623 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001624 if not hook_results.should_continue():
1625 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626
1627 if cmd == 'dcommit':
1628 # Check the tree status if the tree status URL is set.
1629 status = GetTreeStatus()
1630 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001631 print('The tree is closed. Please wait for it to reopen. Use '
1632 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001633 return 1
1634 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001635 print('Unable to determine tree status. Please verify manually and '
1636 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001637 else:
1638 breakpad.SendStack(
1639 'GitClHooksBypassedCommit',
1640 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001641 (cl.GetRietveldServer(), cl.GetIssue()),
1642 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001644 change_desc = ChangeDescription(options.message)
1645 if not change_desc.description and cl.GetIssue():
1646 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001648 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001649 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001650 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001651 else:
1652 print 'No description set.'
1653 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1654 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001656 # Keep a separate copy for the commit message, because the commit message
1657 # contains the link to the Rietveld issue, while the Rietveld message contains
1658 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001659 # Keep a separate copy for the commit message.
1660 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001661 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001662
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001663 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001664 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001665 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001666 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001667 commit_desc.append_footer('Patch from %s.' % options.contributor)
1668
1669 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001670
1671 branches = [base_branch, cl.GetBranchRef()]
1672 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001673 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001674 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001676 # We want to squash all this branch's commits into one commit with the proper
1677 # description. We do this by doing a "reset --soft" to the base branch (which
1678 # keeps the working copy the same), then dcommitting that. If origin/master
1679 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1680 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001682 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1683 # Delete the branches if they exist.
1684 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1685 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1686 result = RunGitWithCode(showref_cmd)
1687 if result[0] == 0:
1688 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689
1690 # We might be in a directory that's present in this branch but not in the
1691 # trunk. Move up to the top of the tree so that git commands that expect a
1692 # valid CWD won't fail after we check out the merge branch.
1693 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1694 if rel_base_path:
1695 os.chdir(rel_base_path)
1696
1697 # Stuff our change into the merge branch.
1698 # We wrap in a try...finally block so if anything goes wrong,
1699 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001700 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001702 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1703 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001705 RunGit(
1706 [
1707 'commit', '--author', options.contributor,
1708 '-m', commit_desc.description,
1709 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001711 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001712 if base_has_submodules:
1713 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1714 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1715 RunGit(['checkout', CHERRY_PICK_BRANCH])
1716 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717 if cmd == 'push':
1718 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001719 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720 retcode, output = RunGitWithCode(
1721 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1722 logging.debug(output)
1723 else:
1724 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001725 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001726 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001727 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728 finally:
1729 # And then swap back to the original branch and clean up.
1730 RunGit(['checkout', '-q', cl.GetBranch()])
1731 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001732 if base_has_submodules:
1733 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001734
1735 if cl.GetIssue():
1736 if cmd == 'dcommit' and 'Committed r' in output:
1737 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1738 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001739 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1740 for l in output.splitlines(False))
1741 match = filter(None, match)
1742 if len(match) != 1:
1743 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1744 output)
1745 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746 else:
1747 return 1
1748 viewvc_url = settings.GetViewVCUrl()
1749 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001750 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001751 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001752 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753 print ('Closing issue '
1754 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001755 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001757 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001758 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001759 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001760 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1761 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001762 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001763
1764 if retcode == 0:
1765 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1766 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001767 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001768
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769 return 0
1770
1771
1772@usage('[upstream branch to apply against]')
1773def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001774 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001775 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001776 message = """This doesn't appear to be an SVN repository.
1777If your project has a git mirror with an upstream SVN master, you probably need
1778to run 'git svn init', see your project's git mirror documentation.
1779If your project has a true writeable upstream repository, you probably want
1780to run 'git cl push' instead.
1781Choose wisely, if you get this wrong, your commit might appear to succeed but
1782will instead be silently ignored."""
1783 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001784 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001785 return SendUpstream(parser, args, 'dcommit')
1786
1787
1788@usage('[upstream branch to apply against]')
1789def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001790 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 if settings.GetIsGitSvn():
1792 print('This appears to be an SVN repository.')
1793 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001794 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 return SendUpstream(parser, args, 'push')
1796
1797
1798@usage('<patch url or issue id>')
1799def CMDpatch(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001800 """Patchs in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801 parser.add_option('-b', dest='newbranch',
1802 help='create a new branch off trunk for the patch')
1803 parser.add_option('-f', action='store_true', dest='force',
1804 help='with -b, clobber any existing branch')
1805 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001806 help='failed patches spew .rej files rather than '
1807 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1809 help="don't commit after patch applies")
1810 (options, args) = parser.parse_args(args)
1811 if len(args) != 1:
1812 parser.print_help()
1813 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001814 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001816 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001817 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001818
maruel@chromium.org52424302012-08-29 15:14:30 +00001819 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001821 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001822 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001823 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001824 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001825 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001826 # Assume it's a URL to the patch. Default to https.
1827 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001828 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001829 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830 DieWithError('Must pass an issue ID or full URL for '
1831 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001832 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001833 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001834 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835
1836 if options.newbranch:
1837 if options.force:
1838 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001839 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 RunGit(['checkout', '-b', options.newbranch,
1841 Changelist().GetUpstreamBranch()])
1842
1843 # Switch up to the top-level directory, if necessary, in preparation for
1844 # applying the patch.
1845 top = RunGit(['rev-parse', '--show-cdup']).strip()
1846 if top:
1847 os.chdir(top)
1848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 # Git patches have a/ at the beginning of source paths. We strip that out
1850 # with a sed script rather than the -p flag to patch so we can feed either
1851 # Git or svn-style patches into the same apply command.
1852 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001853 try:
1854 patch_data = subprocess2.check_output(
1855 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1856 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001857 DieWithError('Git patch mungling failed.')
1858 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001859 env = os.environ.copy()
1860 # 'cat' is a magical git string that disables pagers on all platforms.
1861 env['GIT_PAGER'] = 'cat'
1862
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 # We use "git apply" to apply the patch instead of "patch" so that we can
1864 # pick up file adds.
1865 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001866 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867 if options.reject:
1868 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001869 elif IsGitVersionAtLeast('1.7.12'):
1870 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001871 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001872 subprocess2.check_call(cmd, env=env,
1873 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001874 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875 DieWithError('Failed to apply the patch')
1876
1877 # If we had an issue, commit the current state and register the issue.
1878 if not options.nocommit:
1879 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1880 cl = Changelist()
1881 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001882 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001883 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884 else:
1885 print "Patch applied to index."
1886 return 0
1887
1888
1889def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001890 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001891 # Provide a wrapper for git svn rebase to help avoid accidental
1892 # git svn dcommit.
1893 # It's the only command that doesn't use parser at all since we just defer
1894 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001895 env = os.environ.copy()
1896 # 'cat' is a magical git string that disables pagers on all platforms.
1897 env['GIT_PAGER'] = 'cat'
1898
1899 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001900
1901
1902def GetTreeStatus():
1903 """Fetches the tree status and returns either 'open', 'closed',
1904 'unknown' or 'unset'."""
1905 url = settings.GetTreeStatusUrl(error_ok=True)
1906 if url:
1907 status = urllib2.urlopen(url).read().lower()
1908 if status.find('closed') != -1 or status == '0':
1909 return 'closed'
1910 elif status.find('open') != -1 or status == '1':
1911 return 'open'
1912 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 return 'unset'
1914
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001915
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001916def GetTreeStatusReason():
1917 """Fetches the tree status from a json url and returns the message
1918 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001919 url = settings.GetTreeStatusUrl()
1920 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921 connection = urllib2.urlopen(json_url)
1922 status = json.loads(connection.read())
1923 connection.close()
1924 return status['message']
1925
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001928 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001929 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930 status = GetTreeStatus()
1931 if 'unset' == status:
1932 print 'You must configure your tree status URL by running "git cl config".'
1933 return 2
1934
1935 print "The tree is %s" % status
1936 print
1937 print GetTreeStatusReason()
1938 if status != 'open':
1939 return 1
1940 return 0
1941
1942
maruel@chromium.org15192402012-09-06 12:38:29 +00001943def CMDtry(parser, args):
1944 """Triggers a try job through Rietveld."""
1945 group = optparse.OptionGroup(parser, "Try job options")
1946 group.add_option(
1947 "-b", "--bot", action="append",
1948 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1949 "times to specify multiple builders. ex: "
1950 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1951 "the try server waterfall for the builders name and the tests "
1952 "available. Can also be used to specify gtest_filter, e.g. "
1953 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1954 group.add_option(
1955 "-r", "--revision",
1956 help="Revision to use for the try job; default: the "
1957 "revision will be determined by the try server; see "
1958 "its waterfall for more info")
1959 group.add_option(
1960 "-c", "--clobber", action="store_true", default=False,
1961 help="Force a clobber before building; e.g. don't do an "
1962 "incremental build")
1963 group.add_option(
1964 "--project",
1965 help="Override which project to use. Projects are defined "
1966 "server-side to define what default bot set to use")
1967 group.add_option(
1968 "-t", "--testfilter", action="append", default=[],
1969 help=("Apply a testfilter to all the selected builders. Unless the "
1970 "builders configurations are similar, use multiple "
1971 "--bot <builder>:<test> arguments."))
1972 group.add_option(
1973 "-n", "--name", help="Try job name; default to current branch name")
1974 parser.add_option_group(group)
1975 options, args = parser.parse_args(args)
1976
1977 if args:
1978 parser.error('Unknown arguments: %s' % args)
1979
1980 cl = Changelist()
1981 if not cl.GetIssue():
1982 parser.error('Need to upload first')
1983
1984 if not options.name:
1985 options.name = cl.GetBranch()
1986
1987 # Process --bot and --testfilter.
1988 if not options.bot:
1989 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001990 change = cl.GetChange(
1991 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1992 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001993 options.bot = presubmit_support.DoGetTrySlaves(
1994 change,
1995 change.LocalPaths(),
1996 settings.GetRoot(),
1997 None,
1998 None,
1999 options.verbose,
2000 sys.stdout)
2001 if not options.bot:
2002 parser.error('No default try builder to try, use --bot')
2003
2004 builders_and_tests = {}
2005 for bot in options.bot:
2006 if ':' in bot:
2007 builder, tests = bot.split(':', 1)
2008 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2009 elif ',' in bot:
2010 parser.error('Specify one bot per --bot flag')
2011 else:
2012 builders_and_tests.setdefault(bot, []).append('defaulttests')
2013
2014 if options.testfilter:
2015 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2016 builders_and_tests = dict(
2017 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2018 if t != ['compile'])
2019
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002020 if any('triggered' in b for b in builders_and_tests):
2021 print >> sys.stderr, (
2022 'ERROR You are trying to send a job to a triggered bot. This type of'
2023 ' bot requires an\ninitial job from a parent (usually a builder). '
2024 'Instead send your job to the parent.\n'
2025 'Bot list: %s' % builders_and_tests)
2026 return 1
2027
maruel@chromium.org15192402012-09-06 12:38:29 +00002028 patchset = cl.GetPatchset()
2029 if not cl.GetPatchset():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002030 patchset = cl.GetMostRecentPatchset()
maruel@chromium.org15192402012-09-06 12:38:29 +00002031
2032 cl.RpcServer().trigger_try_jobs(
2033 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2034 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002035 print('Tried jobs on:')
2036 length = max(len(builder) for builder in builders_and_tests)
2037 for builder in sorted(builders_and_tests):
2038 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002039 return 0
2040
2041
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002042@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002044 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002045 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002046 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002047 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002048 return 0
2049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002051 if args:
2052 # One arg means set upstream branch.
2053 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2054 cl = Changelist()
2055 print "Upstream branch set to " + cl.GetUpstreamBranch()
2056 else:
2057 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058 return 0
2059
2060
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002061def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002062 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002063 _, args = parser.parse_args(args)
2064 if args:
2065 parser.error('Unrecognized args: %s' % ' '.join(args))
2066 cl = Changelist()
2067 cl.SetFlag('commit', '1')
2068 return 0
2069
2070
groby@chromium.org411034a2013-02-26 15:12:01 +00002071def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002072 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002073 _, args = parser.parse_args(args)
2074 if args:
2075 parser.error('Unrecognized args: %s' % ' '.join(args))
2076 cl = Changelist()
2077 # Ensure there actually is an issue to close.
2078 cl.GetDescription()
2079 cl.CloseIssue()
2080 return 0
2081
2082
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002083def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002084 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002085 CLANG_EXTS = ['.cc', '.cpp', '.h']
2086 parser.add_option('--full', action='store_true', default=False)
2087 opts, args = parser.parse_args(args)
2088 if args:
2089 parser.error('Unrecognized args: %s' % ' '.join(args))
2090
digit@chromium.org29e47272013-05-17 17:01:46 +00002091 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002092 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002093 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002094 # Only list the names of modified files.
2095 diff_cmd.append('--name-only')
2096 else:
2097 # Only generate context-less patches.
2098 diff_cmd.append('-U0')
2099
2100 # Grab the merge-base commit, i.e. the upstream commit of the current
2101 # branch when it was created or the last time it was rebased. This is
2102 # to cover the case where the user may have called "git fetch origin",
2103 # moving the origin branch to a newer commit, but hasn't rebased yet.
2104 upstream_commit = None
2105 cl = Changelist()
2106 upstream_branch = cl.GetUpstreamBranch()
2107 if upstream_branch:
2108 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2109 upstream_commit = upstream_commit.strip()
2110
2111 if not upstream_commit:
2112 DieWithError('Could not find base commit for this branch. '
2113 'Are you in detached state?')
2114
2115 diff_cmd.append(upstream_commit)
2116
2117 # Handle source file filtering.
2118 diff_cmd.append('--')
2119 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2120 diff_output = RunGit(diff_cmd)
2121
2122 if opts.full:
2123 # diff_output is a list of files to send to clang-format.
2124 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002125 if not files:
2126 print "Nothing to format."
2127 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002128 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002129 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002130 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002131 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2132 'clang-format-diff.py')
2133 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002134 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2135 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2136 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002137
2138 return 0
2139
2140
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002141### Glue code for subcommand handling.
2142
2143
2144def Commands():
2145 """Returns a dict of command and their handling function."""
2146 module = sys.modules[__name__]
2147 cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
2148 return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
2149
2150
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151def Command(name):
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002152 """Retrieves the function to handle a command."""
2153 commands = Commands()
2154 if name in commands:
2155 return commands[name]
2156
2157 # Try to be smart and look if there's something similar.
2158 commands_with_prefix = [c for c in commands if c.startswith(name)]
2159 if len(commands_with_prefix) == 1:
2160 return commands[commands_with_prefix[0]]
2161
2162 # A #closeenough approximation of levenshtein distance.
2163 def close_enough(a, b):
2164 return difflib.SequenceMatcher(a=a, b=b).ratio()
2165
2166 hamming_commands = sorted(
2167 ((close_enough(c, name), c) for c in commands),
2168 reverse=True)
2169 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
2170 # Too ambiguous.
2171 return
2172
2173 if hamming_commands[0][0] < 0.8:
2174 # Not similar enough. Don't be a fool and run a random command.
2175 return
2176
2177 return commands[hamming_commands[0][1]]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002178
2179
2180def CMDhelp(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002181 """Prints list of commands or help for a specific command."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002182 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183 if len(args) == 1:
2184 return main(args + ['--help'])
2185 parser.print_help()
2186 return 0
2187
2188
2189def GenUsage(parser, command):
2190 """Modify an OptParse object with the function's documentation."""
2191 obj = Command(command)
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002192 # Get back the real command name in case Command() guess the actual command
2193 # name.
2194 command = obj.__name__[3:]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195 more = getattr(obj, 'usage_more', '')
2196 if command == 'help':
2197 command = '<command>'
2198 else:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002199 parser.description = obj.__doc__
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2201
2202
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002203class OptionParser(optparse.OptionParser):
2204 """Creates the option parse and add --verbose support."""
2205 def __init__(self, *args, **kwargs):
2206 optparse.OptionParser.__init__(self, *args, **kwargs)
2207 self.add_option(
2208 '-v', '--verbose', action='count', default=0,
2209 help='Use 2 times for more debugging info')
2210
2211 def parse_args(self, args=None, values=None):
2212 options, args = optparse.OptionParser.parse_args(self, args, values)
2213 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2214 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2215 return options, args
2216
2217 def format_description(self, _):
2218 """Disables automatic reformatting."""
2219 lines = self.description.rstrip().splitlines()
2220 lines_fixed = [lines[0]] + [l[2:] if len(l) >= 2 else l for l in lines[1:]]
2221 description = ''.join(l + '\n' for l in lines_fixed)
2222 return description[0].upper() + description[1:]
2223
2224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002225def main(argv):
2226 """Doesn't parse the arguments here, just find the right subcommand to
2227 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002228 if sys.hexversion < 0x02060000:
2229 print >> sys.stderr, (
2230 '\nYour python version %s is unsupported, please upgrade.\n' %
2231 sys.version.split(' ', 1)[0])
2232 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002233
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002234 # Reload settings.
2235 global settings
2236 settings = Settings()
2237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002238 # Do it late so all commands are listed.
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002239 commands = Commands()
2240 length = max(len(c) for c in commands)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002241
2242 def gen_summary(x):
2243 """Creates a oneline summary from the docstring."""
2244 line = x.split('\n', 1)[0].rstrip('.')
2245 return line[0].lower() + line[1:]
2246
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002247 docs = sorted(
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002248 (name, gen_summary(handler.__doc__).strip())
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002249 for name, handler in commands.iteritems())
2250 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
2251 ' %-*s %s' % (length, name, doc) for name, doc in docs))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002252
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002253 parser = OptionParser()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002254 if argv:
2255 command = Command(argv[0])
2256 if command:
2257 # "fix" the usage and the description now that we know the subcommand.
2258 GenUsage(parser, argv[0])
2259 try:
2260 return command(parser, argv[1:])
2261 except urllib2.HTTPError, e:
2262 if e.code != 500:
2263 raise
2264 DieWithError(
2265 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2266 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2267
2268 # Not a known command. Default to help.
2269 GenUsage(parser, 'help')
2270 return CMDhelp(parser, argv)
2271
2272
2273if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002274 # These affect sys.stdout so do it outside of main() to simplify mocks in
2275 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002276 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002277 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002278 sys.exit(main(sys.argv[1:]))