blob: d83365032273d74f16d2f662eaf2713db578b458 [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
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001671 print('Description:')
1672 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673
1674 branches = [base_branch, cl.GetBranchRef()]
1675 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001676 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001677 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001678
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001679 # We want to squash all this branch's commits into one commit with the proper
1680 # description. We do this by doing a "reset --soft" to the base branch (which
1681 # keeps the working copy the same), then dcommitting that. If origin/master
1682 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1683 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001684 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001685 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1686 # Delete the branches if they exist.
1687 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1688 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1689 result = RunGitWithCode(showref_cmd)
1690 if result[0] == 0:
1691 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692
1693 # We might be in a directory that's present in this branch but not in the
1694 # trunk. Move up to the top of the tree so that git commands that expect a
1695 # valid CWD won't fail after we check out the merge branch.
1696 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1697 if rel_base_path:
1698 os.chdir(rel_base_path)
1699
1700 # Stuff our change into the merge branch.
1701 # We wrap in a try...finally block so if anything goes wrong,
1702 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001703 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001705 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1706 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001708 RunGit(
1709 [
1710 'commit', '--author', options.contributor,
1711 '-m', commit_desc.description,
1712 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001714 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001715 if base_has_submodules:
1716 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1717 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1718 RunGit(['checkout', CHERRY_PICK_BRANCH])
1719 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720 if cmd == 'push':
1721 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001722 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723 retcode, output = RunGitWithCode(
1724 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1725 logging.debug(output)
1726 else:
1727 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001728 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001729 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001730 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731 finally:
1732 # And then swap back to the original branch and clean up.
1733 RunGit(['checkout', '-q', cl.GetBranch()])
1734 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001735 if base_has_submodules:
1736 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737
1738 if cl.GetIssue():
1739 if cmd == 'dcommit' and 'Committed r' in output:
1740 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1741 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001742 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1743 for l in output.splitlines(False))
1744 match = filter(None, match)
1745 if len(match) != 1:
1746 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1747 output)
1748 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749 else:
1750 return 1
1751 viewvc_url = settings.GetViewVCUrl()
1752 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001753 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001754 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001755 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 print ('Closing issue '
1757 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001758 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001760 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001761 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001762 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001763 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1764 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001765 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001766
1767 if retcode == 0:
1768 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1769 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001770 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001771
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 return 0
1773
1774
1775@usage('[upstream branch to apply against]')
1776def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001777 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001778 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001779 message = """This doesn't appear to be an SVN repository.
1780If your project has a git mirror with an upstream SVN master, you probably need
1781to run 'git svn init', see your project's git mirror documentation.
1782If your project has a true writeable upstream repository, you probably want
1783to run 'git cl push' instead.
1784Choose wisely, if you get this wrong, your commit might appear to succeed but
1785will instead be silently ignored."""
1786 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001787 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788 return SendUpstream(parser, args, 'dcommit')
1789
1790
1791@usage('[upstream branch to apply against]')
1792def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001793 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794 if settings.GetIsGitSvn():
1795 print('This appears to be an SVN repository.')
1796 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001797 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001798 return SendUpstream(parser, args, 'push')
1799
1800
1801@usage('<patch url or issue id>')
1802def CMDpatch(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001803 """Patchs in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001804 parser.add_option('-b', dest='newbranch',
1805 help='create a new branch off trunk for the patch')
1806 parser.add_option('-f', action='store_true', dest='force',
1807 help='with -b, clobber any existing branch')
1808 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001809 help='failed patches spew .rej files rather than '
1810 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1812 help="don't commit after patch applies")
1813 (options, args) = parser.parse_args(args)
1814 if len(args) != 1:
1815 parser.print_help()
1816 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001817 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001819 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001820 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001821
maruel@chromium.org52424302012-08-29 15:14:30 +00001822 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001823 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001824 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001825 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001826 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001827 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001829 # Assume it's a URL to the patch. Default to https.
1830 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001831 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001832 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 DieWithError('Must pass an issue ID or full URL for '
1834 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001835 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001836 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001837 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838
1839 if options.newbranch:
1840 if options.force:
1841 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001842 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 RunGit(['checkout', '-b', options.newbranch,
1844 Changelist().GetUpstreamBranch()])
1845
1846 # Switch up to the top-level directory, if necessary, in preparation for
1847 # applying the patch.
1848 top = RunGit(['rev-parse', '--show-cdup']).strip()
1849 if top:
1850 os.chdir(top)
1851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001852 # Git patches have a/ at the beginning of source paths. We strip that out
1853 # with a sed script rather than the -p flag to patch so we can feed either
1854 # Git or svn-style patches into the same apply command.
1855 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001856 try:
1857 patch_data = subprocess2.check_output(
1858 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1859 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 DieWithError('Git patch mungling failed.')
1861 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001862 env = os.environ.copy()
1863 # 'cat' is a magical git string that disables pagers on all platforms.
1864 env['GIT_PAGER'] = 'cat'
1865
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001866 # We use "git apply" to apply the patch instead of "patch" so that we can
1867 # pick up file adds.
1868 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001869 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001870 if options.reject:
1871 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001872 elif IsGitVersionAtLeast('1.7.12'):
1873 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001874 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001875 subprocess2.check_call(cmd, env=env,
1876 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001877 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001878 DieWithError('Failed to apply the patch')
1879
1880 # If we had an issue, commit the current state and register the issue.
1881 if not options.nocommit:
1882 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1883 cl = Changelist()
1884 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001885 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001886 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887 else:
1888 print "Patch applied to index."
1889 return 0
1890
1891
1892def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001893 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894 # Provide a wrapper for git svn rebase to help avoid accidental
1895 # git svn dcommit.
1896 # It's the only command that doesn't use parser at all since we just defer
1897 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001898 env = os.environ.copy()
1899 # 'cat' is a magical git string that disables pagers on all platforms.
1900 env['GIT_PAGER'] = 'cat'
1901
1902 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903
1904
1905def GetTreeStatus():
1906 """Fetches the tree status and returns either 'open', 'closed',
1907 'unknown' or 'unset'."""
1908 url = settings.GetTreeStatusUrl(error_ok=True)
1909 if url:
1910 status = urllib2.urlopen(url).read().lower()
1911 if status.find('closed') != -1 or status == '0':
1912 return 'closed'
1913 elif status.find('open') != -1 or status == '1':
1914 return 'open'
1915 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001916 return 'unset'
1917
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919def GetTreeStatusReason():
1920 """Fetches the tree status from a json url and returns the message
1921 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001922 url = settings.GetTreeStatusUrl()
1923 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924 connection = urllib2.urlopen(json_url)
1925 status = json.loads(connection.read())
1926 connection.close()
1927 return status['message']
1928
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001929
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001931 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001932 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 status = GetTreeStatus()
1934 if 'unset' == status:
1935 print 'You must configure your tree status URL by running "git cl config".'
1936 return 2
1937
1938 print "The tree is %s" % status
1939 print
1940 print GetTreeStatusReason()
1941 if status != 'open':
1942 return 1
1943 return 0
1944
1945
maruel@chromium.org15192402012-09-06 12:38:29 +00001946def CMDtry(parser, args):
1947 """Triggers a try job through Rietveld."""
1948 group = optparse.OptionGroup(parser, "Try job options")
1949 group.add_option(
1950 "-b", "--bot", action="append",
1951 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1952 "times to specify multiple builders. ex: "
1953 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1954 "the try server waterfall for the builders name and the tests "
1955 "available. Can also be used to specify gtest_filter, e.g. "
1956 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1957 group.add_option(
1958 "-r", "--revision",
1959 help="Revision to use for the try job; default: the "
1960 "revision will be determined by the try server; see "
1961 "its waterfall for more info")
1962 group.add_option(
1963 "-c", "--clobber", action="store_true", default=False,
1964 help="Force a clobber before building; e.g. don't do an "
1965 "incremental build")
1966 group.add_option(
1967 "--project",
1968 help="Override which project to use. Projects are defined "
1969 "server-side to define what default bot set to use")
1970 group.add_option(
1971 "-t", "--testfilter", action="append", default=[],
1972 help=("Apply a testfilter to all the selected builders. Unless the "
1973 "builders configurations are similar, use multiple "
1974 "--bot <builder>:<test> arguments."))
1975 group.add_option(
1976 "-n", "--name", help="Try job name; default to current branch name")
1977 parser.add_option_group(group)
1978 options, args = parser.parse_args(args)
1979
1980 if args:
1981 parser.error('Unknown arguments: %s' % args)
1982
1983 cl = Changelist()
1984 if not cl.GetIssue():
1985 parser.error('Need to upload first')
1986
1987 if not options.name:
1988 options.name = cl.GetBranch()
1989
1990 # Process --bot and --testfilter.
1991 if not options.bot:
1992 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001993 change = cl.GetChange(
1994 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1995 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001996 options.bot = presubmit_support.DoGetTrySlaves(
1997 change,
1998 change.LocalPaths(),
1999 settings.GetRoot(),
2000 None,
2001 None,
2002 options.verbose,
2003 sys.stdout)
2004 if not options.bot:
2005 parser.error('No default try builder to try, use --bot')
2006
2007 builders_and_tests = {}
2008 for bot in options.bot:
2009 if ':' in bot:
2010 builder, tests = bot.split(':', 1)
2011 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2012 elif ',' in bot:
2013 parser.error('Specify one bot per --bot flag')
2014 else:
2015 builders_and_tests.setdefault(bot, []).append('defaulttests')
2016
2017 if options.testfilter:
2018 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2019 builders_and_tests = dict(
2020 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2021 if t != ['compile'])
2022
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002023 if any('triggered' in b for b in builders_and_tests):
2024 print >> sys.stderr, (
2025 'ERROR You are trying to send a job to a triggered bot. This type of'
2026 ' bot requires an\ninitial job from a parent (usually a builder). '
2027 'Instead send your job to the parent.\n'
2028 'Bot list: %s' % builders_and_tests)
2029 return 1
2030
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002031 patchset = cl.GetMostRecentPatchset()
2032 if patchset and patchset != cl.GetPatchset():
2033 print(
2034 '\nWARNING Mismatch between local config and server. Did a previous '
2035 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2036 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002037
2038 cl.RpcServer().trigger_try_jobs(
2039 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2040 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002041 print('Tried jobs on:')
2042 length = max(len(builder) for builder in builders_and_tests)
2043 for builder in sorted(builders_and_tests):
2044 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002045 return 0
2046
2047
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002048@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002050 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002051 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002052 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002053 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002054 return 0
2055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002057 if args:
2058 # One arg means set upstream branch.
2059 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2060 cl = Changelist()
2061 print "Upstream branch set to " + cl.GetUpstreamBranch()
2062 else:
2063 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064 return 0
2065
2066
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002067def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002068 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002069 _, args = parser.parse_args(args)
2070 if args:
2071 parser.error('Unrecognized args: %s' % ' '.join(args))
2072 cl = Changelist()
2073 cl.SetFlag('commit', '1')
2074 return 0
2075
2076
groby@chromium.org411034a2013-02-26 15:12:01 +00002077def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002078 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002079 _, args = parser.parse_args(args)
2080 if args:
2081 parser.error('Unrecognized args: %s' % ' '.join(args))
2082 cl = Changelist()
2083 # Ensure there actually is an issue to close.
2084 cl.GetDescription()
2085 cl.CloseIssue()
2086 return 0
2087
2088
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002089def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002090 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002091 CLANG_EXTS = ['.cc', '.cpp', '.h']
2092 parser.add_option('--full', action='store_true', default=False)
2093 opts, args = parser.parse_args(args)
2094 if args:
2095 parser.error('Unrecognized args: %s' % ' '.join(args))
2096
digit@chromium.org29e47272013-05-17 17:01:46 +00002097 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002098 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002099 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002100 # Only list the names of modified files.
2101 diff_cmd.append('--name-only')
2102 else:
2103 # Only generate context-less patches.
2104 diff_cmd.append('-U0')
2105
2106 # Grab the merge-base commit, i.e. the upstream commit of the current
2107 # branch when it was created or the last time it was rebased. This is
2108 # to cover the case where the user may have called "git fetch origin",
2109 # moving the origin branch to a newer commit, but hasn't rebased yet.
2110 upstream_commit = None
2111 cl = Changelist()
2112 upstream_branch = cl.GetUpstreamBranch()
2113 if upstream_branch:
2114 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2115 upstream_commit = upstream_commit.strip()
2116
2117 if not upstream_commit:
2118 DieWithError('Could not find base commit for this branch. '
2119 'Are you in detached state?')
2120
2121 diff_cmd.append(upstream_commit)
2122
2123 # Handle source file filtering.
2124 diff_cmd.append('--')
2125 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2126 diff_output = RunGit(diff_cmd)
2127
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002128 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2129
digit@chromium.org29e47272013-05-17 17:01:46 +00002130 if opts.full:
2131 # diff_output is a list of files to send to clang-format.
2132 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002133 if not files:
2134 print "Nothing to format."
2135 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002136 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2137 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002138 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002139 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002140 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2141 'clang-format-diff.py')
2142 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002143 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2144 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002145 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002146
2147 return 0
2148
2149
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002150### Glue code for subcommand handling.
2151
2152
2153def Commands():
2154 """Returns a dict of command and their handling function."""
2155 module = sys.modules[__name__]
2156 cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
2157 return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
2158
2159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002160def Command(name):
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002161 """Retrieves the function to handle a command."""
2162 commands = Commands()
2163 if name in commands:
2164 return commands[name]
2165
2166 # Try to be smart and look if there's something similar.
2167 commands_with_prefix = [c for c in commands if c.startswith(name)]
2168 if len(commands_with_prefix) == 1:
2169 return commands[commands_with_prefix[0]]
2170
2171 # A #closeenough approximation of levenshtein distance.
2172 def close_enough(a, b):
2173 return difflib.SequenceMatcher(a=a, b=b).ratio()
2174
2175 hamming_commands = sorted(
2176 ((close_enough(c, name), c) for c in commands),
2177 reverse=True)
2178 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
2179 # Too ambiguous.
2180 return
2181
2182 if hamming_commands[0][0] < 0.8:
2183 # Not similar enough. Don't be a fool and run a random command.
2184 return
2185
2186 return commands[hamming_commands[0][1]]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187
2188
2189def CMDhelp(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002190 """Prints list of commands or help for a specific command."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002191 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192 if len(args) == 1:
2193 return main(args + ['--help'])
2194 parser.print_help()
2195 return 0
2196
2197
2198def GenUsage(parser, command):
2199 """Modify an OptParse object with the function's documentation."""
2200 obj = Command(command)
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002201 # Get back the real command name in case Command() guess the actual command
2202 # name.
2203 command = obj.__name__[3:]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002204 more = getattr(obj, 'usage_more', '')
2205 if command == 'help':
2206 command = '<command>'
2207 else:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002208 parser.description = obj.__doc__
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002209 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2210
2211
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002212class OptionParser(optparse.OptionParser):
2213 """Creates the option parse and add --verbose support."""
2214 def __init__(self, *args, **kwargs):
2215 optparse.OptionParser.__init__(self, *args, **kwargs)
2216 self.add_option(
2217 '-v', '--verbose', action='count', default=0,
2218 help='Use 2 times for more debugging info')
2219
2220 def parse_args(self, args=None, values=None):
2221 options, args = optparse.OptionParser.parse_args(self, args, values)
2222 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2223 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2224 return options, args
2225
2226 def format_description(self, _):
2227 """Disables automatic reformatting."""
2228 lines = self.description.rstrip().splitlines()
2229 lines_fixed = [lines[0]] + [l[2:] if len(l) >= 2 else l for l in lines[1:]]
2230 description = ''.join(l + '\n' for l in lines_fixed)
2231 return description[0].upper() + description[1:]
2232
2233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002234def main(argv):
2235 """Doesn't parse the arguments here, just find the right subcommand to
2236 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002237 if sys.hexversion < 0x02060000:
2238 print >> sys.stderr, (
2239 '\nYour python version %s is unsupported, please upgrade.\n' %
2240 sys.version.split(' ', 1)[0])
2241 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002242
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002243 # Reload settings.
2244 global settings
2245 settings = Settings()
2246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002247 # Do it late so all commands are listed.
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002248 commands = Commands()
2249 length = max(len(c) for c in commands)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002250
2251 def gen_summary(x):
2252 """Creates a oneline summary from the docstring."""
2253 line = x.split('\n', 1)[0].rstrip('.')
2254 return line[0].lower() + line[1:]
2255
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002256 docs = sorted(
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002257 (name, gen_summary(handler.__doc__).strip())
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002258 for name, handler in commands.iteritems())
2259 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
2260 ' %-*s %s' % (length, name, doc) for name, doc in docs))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002261
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002262 parser = OptionParser()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002263 if argv:
2264 command = Command(argv[0])
2265 if command:
2266 # "fix" the usage and the description now that we know the subcommand.
2267 GenUsage(parser, argv[0])
2268 try:
2269 return command(parser, argv[1:])
2270 except urllib2.HTTPError, e:
2271 if e.code != 500:
2272 raise
2273 DieWithError(
2274 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2275 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2276
2277 # Not a known command. Default to help.
2278 GenUsage(parser, 'help')
2279 return CMDhelp(parser, argv)
2280
2281
2282if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002283 # These affect sys.stdout so do it outside of main() to simplify mocks in
2284 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002285 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002286 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002287 sys.exit(main(sys.argv[1:]))