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