blob: 1dcb1fe8ca7bcc1bb0033997cd035a157f2b9e0c [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.org1033efd2013-07-23 23:25:09 +00001172 print ' %*s: %s%s%s' % (
1173 alignment, ShortBranchName(branch), color, issue, Fore.RESET)
1174
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001175 cl = Changelist()
1176 print
1177 print 'Current branch:',
1178 if not cl.GetIssue():
1179 print 'no issue assigned.'
1180 return 0
1181 print cl.GetBranch()
1182 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1183 print 'Issue description:'
1184 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 return 0
1186
1187
1188@usage('[issue_number]')
1189def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001190 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
1192 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001193 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001194 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195
1196 cl = Changelist()
1197 if len(args) > 0:
1198 try:
1199 issue = int(args[0])
1200 except ValueError:
1201 DieWithError('Pass a number to set the issue or none to list it.\n'
1202 'Maybe you want to run git cl status?')
1203 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001204 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 return 0
1206
1207
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001208def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001209 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001210 (_, args) = parser.parse_args(args)
1211 if args:
1212 parser.error('Unsupported argument: %s' % args)
1213
1214 cl = Changelist()
1215 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001216 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001217 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001218 if message['disapproval']:
1219 color = Fore.RED
1220 elif message['approval']:
1221 color = Fore.GREEN
1222 elif message['sender'] == data['owner_email']:
1223 color = Fore.MAGENTA
1224 else:
1225 color = Fore.BLUE
1226 print '\n%s%s %s%s' % (
1227 color, message['date'].split('.', 1)[0], message['sender'],
1228 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001229 if message['text'].strip():
1230 print '\n'.join(' ' + l for l in message['text'].splitlines())
1231 return 0
1232
1233
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001234def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001235 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001236 cl = Changelist()
1237 if not cl.GetIssue():
1238 DieWithError('This branch has no associated changelist.')
1239 description = ChangeDescription(cl.GetDescription())
1240 description.prompt()
1241 cl.UpdateDescription(description.description)
1242 return 0
1243
1244
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245def CreateDescriptionFromLog(args):
1246 """Pulls out the commit log to use as a base for the CL description."""
1247 log_args = []
1248 if len(args) == 1 and not args[0].endswith('.'):
1249 log_args = [args[0] + '..']
1250 elif len(args) == 1 and args[0].endswith('...'):
1251 log_args = [args[0][:-1]]
1252 elif len(args) == 2:
1253 log_args = [args[0] + '..' + args[1]]
1254 else:
1255 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001256 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257
1258
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001260 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001261 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001263 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001264 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 (options, args) = parser.parse_args(args)
1266
ukai@chromium.org259e4682012-10-25 07:36:33 +00001267 if not options.force and is_dirty_git_tree('presubmit'):
1268 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 return 1
1270
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001271 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 if args:
1273 base_branch = args[0]
1274 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 # Default to diffing against the common ancestor of the upstream branch.
1276 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001278 cl.RunHook(
1279 committing=not options.upload,
1280 may_prompt=False,
1281 verbose=options.verbose,
1282 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001283 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284
1285
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001286def AddChangeIdToCommitMessage(options, args):
1287 """Re-commits using the current message, assumes the commit hook is in
1288 place.
1289 """
1290 log_desc = options.message or CreateDescriptionFromLog(args)
1291 git_command = ['commit', '--amend', '-m', log_desc]
1292 RunGit(git_command)
1293 new_log_desc = CreateDescriptionFromLog(args)
1294 if CHANGE_ID in new_log_desc:
1295 print 'git-cl: Added Change-Id to commit message.'
1296 else:
1297 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1298
1299
ukai@chromium.orge8077812012-02-03 03:41:46 +00001300def GerritUpload(options, args, cl):
1301 """upload the current branch to gerrit."""
1302 # We assume the remote called "origin" is the one we want.
1303 # It is probably not worthwhile to support different workflows.
1304 remote = 'origin'
1305 branch = 'master'
1306 if options.target_branch:
1307 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001309 change_desc = ChangeDescription(
1310 options.message or CreateDescriptionFromLog(args))
1311 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001312 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001314 if CHANGE_ID not in change_desc.description:
1315 AddChangeIdToCommitMessage(options, args)
1316 if options.reviewers:
1317 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318
ukai@chromium.orge8077812012-02-03 03:41:46 +00001319 receive_options = []
1320 cc = cl.GetCCList().split(',')
1321 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001322 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001323 cc = filter(None, cc)
1324 if cc:
1325 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001326 if change_desc.get_reviewers():
1327 receive_options.extend(
1328 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329
ukai@chromium.orge8077812012-02-03 03:41:46 +00001330 git_command = ['push']
1331 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001332 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001333 ' '.join(receive_options))
1334 git_command += [remote, 'HEAD:refs/for/' + branch]
1335 RunGit(git_command)
1336 # TODO(ukai): parse Change-Id: and set issue number?
1337 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001338
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339
ukai@chromium.orge8077812012-02-03 03:41:46 +00001340def RietveldUpload(options, args, cl):
1341 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1343 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 if options.emulate_svn_auto_props:
1345 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346
1347 change_desc = None
1348
1349 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001350 if options.title:
1351 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001352 if options.message:
1353 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001354 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 print ("This branch is associated with issue %s. "
1356 "Adding patch to that issue." % cl.GetIssue())
1357 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001358 if options.title:
1359 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001360 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001361 change_desc = ChangeDescription(message)
1362 if options.reviewers:
1363 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001364 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001365 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001366
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001367 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368 print "Description is empty; aborting."
1369 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001370
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001371 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372 if change_desc.get_reviewers():
1373 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001374 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001375 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001376 DieWithError("Must specify reviewers to send email.")
1377 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001378 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001379 if cc:
1380 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001382 if options.private or settings.GetDefaultPrivateFlag() == "True":
1383 upload_args.append('--private')
1384
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001385 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001386 if not options.find_copies:
1387 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 # Include the upstream repo's URL in the change -- this is useful for
1390 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001391 remote_url = cl.GetGitBaseUrlFromConfig()
1392 if not remote_url:
1393 if settings.GetIsGitSvn():
1394 # URL is dependent on the current directory.
1395 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1396 if data:
1397 keys = dict(line.split(': ', 1) for line in data.splitlines()
1398 if ': ' in line)
1399 remote_url = keys.get('URL', None)
1400 else:
1401 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1402 remote_url = (cl.GetRemoteUrl() + '@'
1403 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 if remote_url:
1405 upload_args.extend(['--base_url', remote_url])
1406
1407 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001408 upload_args = ['upload'] + upload_args + args
1409 logging.info('upload.RealMain(%s)', upload_args)
1410 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001411 except KeyboardInterrupt:
1412 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 except:
1414 # If we got an exception after the user typed a description for their
1415 # change, back up the description before re-raising.
1416 if change_desc:
1417 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1418 print '\nGot exception while uploading -- saving description to %s\n' \
1419 % backup_path
1420 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001421 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 backup_file.close()
1423 raise
1424
1425 if not cl.GetIssue():
1426 cl.SetIssue(issue)
1427 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001428
1429 if options.use_commit_queue:
1430 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 return 0
1432
1433
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001434def cleanup_list(l):
1435 """Fixes a list so that comma separated items are put as individual items.
1436
1437 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1438 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1439 """
1440 items = sum((i.split(',') for i in l), [])
1441 stripped_items = (i.strip() for i in items)
1442 return sorted(filter(None, stripped_items))
1443
1444
ukai@chromium.orge8077812012-02-03 03:41:46 +00001445@usage('[args to "git diff"]')
1446def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001447 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001448 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1449 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001450 parser.add_option('--bypass-watchlists', action='store_true',
1451 dest='bypass_watchlists',
1452 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001453 parser.add_option('-f', action='store_true', dest='force',
1454 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001455 parser.add_option('-m', dest='message', help='message for patchset')
1456 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001457 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001458 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001459 help='reviewer email addresses')
1460 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001461 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001462 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001463 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001464 help='send email to reviewer immediately')
1465 parser.add_option("--emulate_svn_auto_props", action="store_true",
1466 dest="emulate_svn_auto_props",
1467 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001468 parser.add_option('-c', '--use-commit-queue', action='store_true',
1469 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001470 parser.add_option('--private', action='store_true',
1471 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001472 parser.add_option('--target_branch',
1473 help='When uploading to gerrit, remote branch to '
1474 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001475 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001476 (options, args) = parser.parse_args(args)
1477
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001478 if options.target_branch and not settings.GetIsGerrit():
1479 parser.error('Use --target_branch for non gerrit repository.')
1480
ukai@chromium.org259e4682012-10-25 07:36:33 +00001481 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001482 return 1
1483
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001484 options.reviewers = cleanup_list(options.reviewers)
1485 options.cc = cleanup_list(options.cc)
1486
ukai@chromium.orge8077812012-02-03 03:41:46 +00001487 cl = Changelist()
1488 if args:
1489 # TODO(ukai): is it ok for gerrit case?
1490 base_branch = args[0]
1491 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001492 # Default to diffing against common ancestor of upstream branch
1493 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001494 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001495
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001496 # Apply watchlists on upload.
1497 change = cl.GetChange(base_branch, None)
1498 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1499 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001500 if not options.bypass_watchlists:
1501 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001502
ukai@chromium.orge8077812012-02-03 03:41:46 +00001503 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001504 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001505 may_prompt=not options.force,
1506 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001507 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001508 if not hook_results.should_continue():
1509 return 1
1510 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001511 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001512
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001513 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001514 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001515 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001516 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001517 print ('The last upload made from this repository was patchset #%d but '
1518 'the most recent patchset on the server is #%d.'
1519 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001520 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1521 'from another machine or branch the patch you\'re uploading now '
1522 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001523 ask_for_data('About to upload; enter to confirm.')
1524
iannucci@chromium.org79540052012-10-19 23:15:26 +00001525 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001526 if settings.GetIsGerrit():
1527 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001528 ret = RietveldUpload(options, args, cl)
1529 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001530 git_set_branch_value('last-upload-hash',
1531 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001532
1533 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001534
1535
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001536def IsSubmoduleMergeCommit(ref):
1537 # When submodules are added to the repo, we expect there to be a single
1538 # non-git-svn merge commit at remote HEAD with a signature comment.
1539 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001540 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001541 return RunGit(cmd) != ''
1542
1543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544def SendUpstream(parser, args, cmd):
1545 """Common code for CmdPush and CmdDCommit
1546
1547 Squashed commit into a single.
1548 Updates changelog with metadata (e.g. pointer to review).
1549 Pushes/dcommits the code upstream.
1550 Updates review and closes.
1551 """
1552 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1553 help='bypass upload presubmit hook')
1554 parser.add_option('-m', dest='message',
1555 help="override review description")
1556 parser.add_option('-f', action='store_true', dest='force',
1557 help="force yes to questions (don't prompt)")
1558 parser.add_option('-c', dest='contributor',
1559 help="external contributor for patch (appended to " +
1560 "description and used as author for git). Should be " +
1561 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001562 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 (options, args) = parser.parse_args(args)
1564 cl = Changelist()
1565
1566 if not args or cmd == 'push':
1567 # Default to merging against our best guess of the upstream branch.
1568 args = [cl.GetUpstreamBranch()]
1569
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001570 if options.contributor:
1571 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1572 print "Please provide contibutor as 'First Last <email@example.com>'"
1573 return 1
1574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001576 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577
ukai@chromium.org259e4682012-10-25 07:36:33 +00001578 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579 return 1
1580
1581 # This rev-list syntax means "show all commits not in my branch that
1582 # are in base_branch".
1583 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1584 base_branch]).splitlines()
1585 if upstream_commits:
1586 print ('Base branch "%s" has %d commits '
1587 'not in this branch.' % (base_branch, len(upstream_commits)))
1588 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1589 return 1
1590
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001591 # This is the revision `svn dcommit` will commit on top of.
1592 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1593 '--pretty=format:%H'])
1594
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001595 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001596 # If the base_head is a submodule merge commit, the first parent of the
1597 # base_head should be a git-svn commit, which is what we're interested in.
1598 base_svn_head = base_branch
1599 if base_has_submodules:
1600 base_svn_head += '^1'
1601
1602 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001603 if extra_commits:
1604 print ('This branch has %d additional commits not upstreamed yet.'
1605 % len(extra_commits.splitlines()))
1606 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1607 'before attempting to %s.' % (base_branch, cmd))
1608 return 1
1609
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001610 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001611 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001612 author = None
1613 if options.contributor:
1614 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001615 hook_results = cl.RunHook(
1616 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001617 may_prompt=not options.force,
1618 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001619 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001620 if not hook_results.should_continue():
1621 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622
1623 if cmd == 'dcommit':
1624 # Check the tree status if the tree status URL is set.
1625 status = GetTreeStatus()
1626 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001627 print('The tree is closed. Please wait for it to reopen. Use '
1628 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001629 return 1
1630 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001631 print('Unable to determine tree status. Please verify manually and '
1632 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001633 else:
1634 breakpad.SendStack(
1635 'GitClHooksBypassedCommit',
1636 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001637 (cl.GetRietveldServer(), cl.GetIssue()),
1638 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001640 change_desc = ChangeDescription(options.message)
1641 if not change_desc.description and cl.GetIssue():
1642 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001644 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001645 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001646 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001647 else:
1648 print 'No description set.'
1649 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1650 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001652 # Keep a separate copy for the commit message, because the commit message
1653 # contains the link to the Rietveld issue, while the Rietveld message contains
1654 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001655 # Keep a separate copy for the commit message.
1656 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001657 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001658
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001659 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001660 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001661 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001663 commit_desc.append_footer('Patch from %s.' % options.contributor)
1664
1665 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001666
1667 branches = [base_branch, cl.GetBranchRef()]
1668 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001669 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001670 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001671
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001672 # We want to squash all this branch's commits into one commit with the proper
1673 # description. We do this by doing a "reset --soft" to the base branch (which
1674 # keeps the working copy the same), then dcommitting that. If origin/master
1675 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1676 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001678 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1679 # Delete the branches if they exist.
1680 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1681 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1682 result = RunGitWithCode(showref_cmd)
1683 if result[0] == 0:
1684 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001685
1686 # We might be in a directory that's present in this branch but not in the
1687 # trunk. Move up to the top of the tree so that git commands that expect a
1688 # valid CWD won't fail after we check out the merge branch.
1689 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1690 if rel_base_path:
1691 os.chdir(rel_base_path)
1692
1693 # Stuff our change into the merge branch.
1694 # We wrap in a try...finally block so if anything goes wrong,
1695 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001696 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001697 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001698 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1699 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001701 RunGit(
1702 [
1703 'commit', '--author', options.contributor,
1704 '-m', commit_desc.description,
1705 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001707 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001708 if base_has_submodules:
1709 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1710 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1711 RunGit(['checkout', CHERRY_PICK_BRANCH])
1712 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713 if cmd == 'push':
1714 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001715 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716 retcode, output = RunGitWithCode(
1717 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1718 logging.debug(output)
1719 else:
1720 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001721 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001722 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001723 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724 finally:
1725 # And then swap back to the original branch and clean up.
1726 RunGit(['checkout', '-q', cl.GetBranch()])
1727 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001728 if base_has_submodules:
1729 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730
1731 if cl.GetIssue():
1732 if cmd == 'dcommit' and 'Committed r' in output:
1733 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1734 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001735 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1736 for l in output.splitlines(False))
1737 match = filter(None, match)
1738 if len(match) != 1:
1739 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1740 output)
1741 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742 else:
1743 return 1
1744 viewvc_url = settings.GetViewVCUrl()
1745 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001746 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001747 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001748 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749 print ('Closing issue '
1750 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001751 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001752 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001753 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001754 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001755 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001756 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1757 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001758 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001759
1760 if retcode == 0:
1761 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1762 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001763 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 return 0
1766
1767
1768@usage('[upstream branch to apply against]')
1769def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001770 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001772 message = """This doesn't appear to be an SVN repository.
1773If your project has a git mirror with an upstream SVN master, you probably need
1774to run 'git svn init', see your project's git mirror documentation.
1775If your project has a true writeable upstream repository, you probably want
1776to run 'git cl push' instead.
1777Choose wisely, if you get this wrong, your commit might appear to succeed but
1778will instead be silently ignored."""
1779 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001780 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781 return SendUpstream(parser, args, 'dcommit')
1782
1783
1784@usage('[upstream branch to apply against]')
1785def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001786 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001787 if settings.GetIsGitSvn():
1788 print('This appears to be an SVN repository.')
1789 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001790 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 return SendUpstream(parser, args, 'push')
1792
1793
1794@usage('<patch url or issue id>')
1795def CMDpatch(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001796 """Patchs in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797 parser.add_option('-b', dest='newbranch',
1798 help='create a new branch off trunk for the patch')
1799 parser.add_option('-f', action='store_true', dest='force',
1800 help='with -b, clobber any existing branch')
1801 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001802 help='failed patches spew .rej files rather than '
1803 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001804 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1805 help="don't commit after patch applies")
1806 (options, args) = parser.parse_args(args)
1807 if len(args) != 1:
1808 parser.print_help()
1809 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001810 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001812 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001813 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001814
maruel@chromium.org52424302012-08-29 15:14:30 +00001815 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001816 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001817 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001818 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001819 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001820 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001821 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001822 # Assume it's a URL to the patch. Default to https.
1823 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001824 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001825 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001826 DieWithError('Must pass an issue ID or full URL for '
1827 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001828 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001829 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001830 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831
1832 if options.newbranch:
1833 if options.force:
1834 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001835 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 RunGit(['checkout', '-b', options.newbranch,
1837 Changelist().GetUpstreamBranch()])
1838
1839 # Switch up to the top-level directory, if necessary, in preparation for
1840 # applying the patch.
1841 top = RunGit(['rev-parse', '--show-cdup']).strip()
1842 if top:
1843 os.chdir(top)
1844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 # Git patches have a/ at the beginning of source paths. We strip that out
1846 # with a sed script rather than the -p flag to patch so we can feed either
1847 # Git or svn-style patches into the same apply command.
1848 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001849 try:
1850 patch_data = subprocess2.check_output(
1851 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1852 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853 DieWithError('Git patch mungling failed.')
1854 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001855 env = os.environ.copy()
1856 # 'cat' is a magical git string that disables pagers on all platforms.
1857 env['GIT_PAGER'] = 'cat'
1858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859 # We use "git apply" to apply the patch instead of "patch" so that we can
1860 # pick up file adds.
1861 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001862 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 if options.reject:
1864 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001865 elif IsGitVersionAtLeast('1.7.12'):
1866 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001867 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001868 subprocess2.check_call(cmd, env=env,
1869 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001870 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 DieWithError('Failed to apply the patch')
1872
1873 # If we had an issue, commit the current state and register the issue.
1874 if not options.nocommit:
1875 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1876 cl = Changelist()
1877 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001878 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001879 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880 else:
1881 print "Patch applied to index."
1882 return 0
1883
1884
1885def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001886 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887 # Provide a wrapper for git svn rebase to help avoid accidental
1888 # git svn dcommit.
1889 # It's the only command that doesn't use parser at all since we just defer
1890 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001891 env = os.environ.copy()
1892 # 'cat' is a magical git string that disables pagers on all platforms.
1893 env['GIT_PAGER'] = 'cat'
1894
1895 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896
1897
1898def GetTreeStatus():
1899 """Fetches the tree status and returns either 'open', 'closed',
1900 'unknown' or 'unset'."""
1901 url = settings.GetTreeStatusUrl(error_ok=True)
1902 if url:
1903 status = urllib2.urlopen(url).read().lower()
1904 if status.find('closed') != -1 or status == '0':
1905 return 'closed'
1906 elif status.find('open') != -1 or status == '1':
1907 return 'open'
1908 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 return 'unset'
1910
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912def GetTreeStatusReason():
1913 """Fetches the tree status from a json url and returns the message
1914 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001915 url = settings.GetTreeStatusUrl()
1916 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001917 connection = urllib2.urlopen(json_url)
1918 status = json.loads(connection.read())
1919 connection.close()
1920 return status['message']
1921
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001924 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001925 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926 status = GetTreeStatus()
1927 if 'unset' == status:
1928 print 'You must configure your tree status URL by running "git cl config".'
1929 return 2
1930
1931 print "The tree is %s" % status
1932 print
1933 print GetTreeStatusReason()
1934 if status != 'open':
1935 return 1
1936 return 0
1937
1938
maruel@chromium.org15192402012-09-06 12:38:29 +00001939def CMDtry(parser, args):
1940 """Triggers a try job through Rietveld."""
1941 group = optparse.OptionGroup(parser, "Try job options")
1942 group.add_option(
1943 "-b", "--bot", action="append",
1944 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1945 "times to specify multiple builders. ex: "
1946 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1947 "the try server waterfall for the builders name and the tests "
1948 "available. Can also be used to specify gtest_filter, e.g. "
1949 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1950 group.add_option(
1951 "-r", "--revision",
1952 help="Revision to use for the try job; default: the "
1953 "revision will be determined by the try server; see "
1954 "its waterfall for more info")
1955 group.add_option(
1956 "-c", "--clobber", action="store_true", default=False,
1957 help="Force a clobber before building; e.g. don't do an "
1958 "incremental build")
1959 group.add_option(
1960 "--project",
1961 help="Override which project to use. Projects are defined "
1962 "server-side to define what default bot set to use")
1963 group.add_option(
1964 "-t", "--testfilter", action="append", default=[],
1965 help=("Apply a testfilter to all the selected builders. Unless the "
1966 "builders configurations are similar, use multiple "
1967 "--bot <builder>:<test> arguments."))
1968 group.add_option(
1969 "-n", "--name", help="Try job name; default to current branch name")
1970 parser.add_option_group(group)
1971 options, args = parser.parse_args(args)
1972
1973 if args:
1974 parser.error('Unknown arguments: %s' % args)
1975
1976 cl = Changelist()
1977 if not cl.GetIssue():
1978 parser.error('Need to upload first')
1979
1980 if not options.name:
1981 options.name = cl.GetBranch()
1982
1983 # Process --bot and --testfilter.
1984 if not options.bot:
1985 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001986 change = cl.GetChange(
1987 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1988 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001989 options.bot = presubmit_support.DoGetTrySlaves(
1990 change,
1991 change.LocalPaths(),
1992 settings.GetRoot(),
1993 None,
1994 None,
1995 options.verbose,
1996 sys.stdout)
1997 if not options.bot:
1998 parser.error('No default try builder to try, use --bot')
1999
2000 builders_and_tests = {}
2001 for bot in options.bot:
2002 if ':' in bot:
2003 builder, tests = bot.split(':', 1)
2004 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2005 elif ',' in bot:
2006 parser.error('Specify one bot per --bot flag')
2007 else:
2008 builders_and_tests.setdefault(bot, []).append('defaulttests')
2009
2010 if options.testfilter:
2011 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2012 builders_and_tests = dict(
2013 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2014 if t != ['compile'])
2015
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002016 if any('triggered' in b for b in builders_and_tests):
2017 print >> sys.stderr, (
2018 'ERROR You are trying to send a job to a triggered bot. This type of'
2019 ' bot requires an\ninitial job from a parent (usually a builder). '
2020 'Instead send your job to the parent.\n'
2021 'Bot list: %s' % builders_and_tests)
2022 return 1
2023
maruel@chromium.org15192402012-09-06 12:38:29 +00002024 patchset = cl.GetPatchset()
2025 if not cl.GetPatchset():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002026 patchset = cl.GetMostRecentPatchset()
maruel@chromium.org15192402012-09-06 12:38:29 +00002027
2028 cl.RpcServer().trigger_try_jobs(
2029 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2030 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002031 print('Tried jobs on:')
2032 length = max(len(builder) for builder in builders_and_tests)
2033 for builder in sorted(builders_and_tests):
2034 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002035 return 0
2036
2037
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002038@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002040 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002041 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002042 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002043 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002044 return 0
2045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002047 if args:
2048 # One arg means set upstream branch.
2049 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2050 cl = Changelist()
2051 print "Upstream branch set to " + cl.GetUpstreamBranch()
2052 else:
2053 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054 return 0
2055
2056
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002057def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002058 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002059 _, args = parser.parse_args(args)
2060 if args:
2061 parser.error('Unrecognized args: %s' % ' '.join(args))
2062 cl = Changelist()
2063 cl.SetFlag('commit', '1')
2064 return 0
2065
2066
groby@chromium.org411034a2013-02-26 15:12:01 +00002067def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002068 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002069 _, args = parser.parse_args(args)
2070 if args:
2071 parser.error('Unrecognized args: %s' % ' '.join(args))
2072 cl = Changelist()
2073 # Ensure there actually is an issue to close.
2074 cl.GetDescription()
2075 cl.CloseIssue()
2076 return 0
2077
2078
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002079def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002080 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002081 CLANG_EXTS = ['.cc', '.cpp', '.h']
2082 parser.add_option('--full', action='store_true', default=False)
2083 opts, args = parser.parse_args(args)
2084 if args:
2085 parser.error('Unrecognized args: %s' % ' '.join(args))
2086
digit@chromium.org29e47272013-05-17 17:01:46 +00002087 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002088 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002089 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002090 # Only list the names of modified files.
2091 diff_cmd.append('--name-only')
2092 else:
2093 # Only generate context-less patches.
2094 diff_cmd.append('-U0')
2095
2096 # Grab the merge-base commit, i.e. the upstream commit of the current
2097 # branch when it was created or the last time it was rebased. This is
2098 # to cover the case where the user may have called "git fetch origin",
2099 # moving the origin branch to a newer commit, but hasn't rebased yet.
2100 upstream_commit = None
2101 cl = Changelist()
2102 upstream_branch = cl.GetUpstreamBranch()
2103 if upstream_branch:
2104 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2105 upstream_commit = upstream_commit.strip()
2106
2107 if not upstream_commit:
2108 DieWithError('Could not find base commit for this branch. '
2109 'Are you in detached state?')
2110
2111 diff_cmd.append(upstream_commit)
2112
2113 # Handle source file filtering.
2114 diff_cmd.append('--')
2115 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2116 diff_output = RunGit(diff_cmd)
2117
2118 if opts.full:
2119 # diff_output is a list of files to send to clang-format.
2120 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002121 if not files:
2122 print "Nothing to format."
2123 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002124 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002125 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002126 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002127 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2128 'clang-format-diff.py')
2129 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002130 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2131 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2132 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002133
2134 return 0
2135
2136
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002137### Glue code for subcommand handling.
2138
2139
2140def Commands():
2141 """Returns a dict of command and their handling function."""
2142 module = sys.modules[__name__]
2143 cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
2144 return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
2145
2146
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147def Command(name):
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002148 """Retrieves the function to handle a command."""
2149 commands = Commands()
2150 if name in commands:
2151 return commands[name]
2152
2153 # Try to be smart and look if there's something similar.
2154 commands_with_prefix = [c for c in commands if c.startswith(name)]
2155 if len(commands_with_prefix) == 1:
2156 return commands[commands_with_prefix[0]]
2157
2158 # A #closeenough approximation of levenshtein distance.
2159 def close_enough(a, b):
2160 return difflib.SequenceMatcher(a=a, b=b).ratio()
2161
2162 hamming_commands = sorted(
2163 ((close_enough(c, name), c) for c in commands),
2164 reverse=True)
2165 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
2166 # Too ambiguous.
2167 return
2168
2169 if hamming_commands[0][0] < 0.8:
2170 # Not similar enough. Don't be a fool and run a random command.
2171 return
2172
2173 return commands[hamming_commands[0][1]]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002174
2175
2176def CMDhelp(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002177 """Prints list of commands or help for a specific command."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002178 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002179 if len(args) == 1:
2180 return main(args + ['--help'])
2181 parser.print_help()
2182 return 0
2183
2184
2185def GenUsage(parser, command):
2186 """Modify an OptParse object with the function's documentation."""
2187 obj = Command(command)
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002188 # Get back the real command name in case Command() guess the actual command
2189 # name.
2190 command = obj.__name__[3:]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002191 more = getattr(obj, 'usage_more', '')
2192 if command == 'help':
2193 command = '<command>'
2194 else:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002195 parser.description = obj.__doc__
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002196 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2197
2198
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002199class OptionParser(optparse.OptionParser):
2200 """Creates the option parse and add --verbose support."""
2201 def __init__(self, *args, **kwargs):
2202 optparse.OptionParser.__init__(self, *args, **kwargs)
2203 self.add_option(
2204 '-v', '--verbose', action='count', default=0,
2205 help='Use 2 times for more debugging info')
2206
2207 def parse_args(self, args=None, values=None):
2208 options, args = optparse.OptionParser.parse_args(self, args, values)
2209 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2210 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2211 return options, args
2212
2213 def format_description(self, _):
2214 """Disables automatic reformatting."""
2215 lines = self.description.rstrip().splitlines()
2216 lines_fixed = [lines[0]] + [l[2:] if len(l) >= 2 else l for l in lines[1:]]
2217 description = ''.join(l + '\n' for l in lines_fixed)
2218 return description[0].upper() + description[1:]
2219
2220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002221def main(argv):
2222 """Doesn't parse the arguments here, just find the right subcommand to
2223 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002224 if sys.hexversion < 0x02060000:
2225 print >> sys.stderr, (
2226 '\nYour python version %s is unsupported, please upgrade.\n' %
2227 sys.version.split(' ', 1)[0])
2228 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002229
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002230 # Reload settings.
2231 global settings
2232 settings = Settings()
2233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002234 # Do it late so all commands are listed.
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002235 commands = Commands()
2236 length = max(len(c) for c in commands)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002237
2238 def gen_summary(x):
2239 """Creates a oneline summary from the docstring."""
2240 line = x.split('\n', 1)[0].rstrip('.')
2241 return line[0].lower() + line[1:]
2242
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002243 docs = sorted(
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002244 (name, gen_summary(handler.__doc__).strip())
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002245 for name, handler in commands.iteritems())
2246 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
2247 ' %-*s %s' % (length, name, doc) for name, doc in docs))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002248
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002249 parser = OptionParser()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002250 if argv:
2251 command = Command(argv[0])
2252 if command:
2253 # "fix" the usage and the description now that we know the subcommand.
2254 GenUsage(parser, argv[0])
2255 try:
2256 return command(parser, argv[1:])
2257 except urllib2.HTTPError, e:
2258 if e.code != 500:
2259 raise
2260 DieWithError(
2261 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2262 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2263
2264 # Not a known command. Default to help.
2265 GenUsage(parser, 'help')
2266 return CMDhelp(parser, argv)
2267
2268
2269if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002270 # These affect sys.stdout so do it outside of main() to simplify mocks in
2271 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002272 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002273 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002274 sys.exit(main(sys.argv[1:]))