blob: c401f0e762f20e421906e16a2fa4f2ac67a02783 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
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
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000035import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000036import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000038import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000040import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000041import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000043import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000047DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000048POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000050GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000051CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000053# Shortcut since it quickly becomes redundant.
54Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000055
maruel@chromium.orgddd59412011-11-30 14:20:38 +000056# Initialized in main()
57settings = None
58
59
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000061 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062 sys.exit(1)
63
64
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000067 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000068 except subprocess2.CalledProcessError as e:
69 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 'Command "%s" failed.\n%s' % (
73 ' '.join(args), error_message or e.stdout or ''))
74 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
76
77def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000079 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000084 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000085 env = os.environ.copy()
86 # 'cat' is a magical git string that disables pagers on all platforms.
87 env['GIT_PAGER'] = 'cat'
88 out, code = subprocess2.communicate(['git'] + args,
89 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000090 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000091 return code, out[0]
92 except ValueError:
93 # When the subprocess fails, it returns None. That triggers a ValueError
94 # when trying to unpack the return value into (out, code).
95 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096
97
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000098def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000099 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000100 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000101 return (version.startswith(prefix) and
102 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000103
104
maruel@chromium.org90541732011-04-01 17:54:18 +0000105def ask_for_data(prompt):
106 try:
107 return raw_input(prompt)
108 except KeyboardInterrupt:
109 # Hide the exception.
110 sys.exit(1)
111
112
iannucci@chromium.org79540052012-10-19 23:15:26 +0000113def git_set_branch_value(key, value):
114 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000115 if not branch:
116 return
117
118 cmd = ['config']
119 if isinstance(value, int):
120 cmd.append('--int')
121 git_key = 'branch.%s.%s' % (branch, key)
122 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000123
124
125def git_get_branch_default(key, default):
126 branch = Changelist().GetBranch()
127 if branch:
128 git_key = 'branch.%s.%s' % (branch, key)
129 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
130 try:
131 return int(stdout.strip())
132 except ValueError:
133 pass
134 return default
135
136
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137def add_git_similarity(parser):
138 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000140 help='Sets the percentage that a pair of files need to match in order to'
141 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 parser.add_option(
143 '--find-copies', action='store_true',
144 help='Allows git to look for copies.')
145 parser.add_option(
146 '--no-find-copies', action='store_false', dest='find_copies',
147 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000148
149 old_parser_args = parser.parse_args
150 def Parse(args):
151 options, args = old_parser_args(args)
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156 print('Note: Saving similarity of %d%% in git config.'
157 % options.similarity)
158 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160 options.similarity = max(0, min(options.similarity, 100))
161
162 if options.find_copies is None:
163 options.find_copies = bool(
164 git_get_branch_default('git-find-copies', True))
165 else:
166 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000167
168 print('Using %d%% similarity for rename/copy detection. '
169 'Override with --similarity.' % options.similarity)
170
171 return options, args
172 parser.parse_args = Parse
173
174
ukai@chromium.org259e4682012-10-25 07:36:33 +0000175def is_dirty_git_tree(cmd):
176 # Make sure index is up-to-date before running diff-index.
177 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
178 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
179 if dirty:
180 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
181 print 'Uncommitted files: (git diff-index --name-status HEAD)'
182 print dirty[:4096]
183 if len(dirty) > 4096:
184 print '... (run "git diff-index --name-status HEAD" to see full output).'
185 return True
186 return False
187
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000188
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000189def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
190 """Return the corresponding git ref if |base_url| together with |glob_spec|
191 matches the full |url|.
192
193 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
194 """
195 fetch_suburl, as_ref = glob_spec.split(':')
196 if allow_wildcards:
197 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
198 if glob_match:
199 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
200 # "branches/{472,597,648}/src:refs/remotes/svn/*".
201 branch_re = re.escape(base_url)
202 if glob_match.group(1):
203 branch_re += '/' + re.escape(glob_match.group(1))
204 wildcard = glob_match.group(2)
205 if wildcard == '*':
206 branch_re += '([^/]*)'
207 else:
208 # Escape and replace surrounding braces with parentheses and commas
209 # with pipe symbols.
210 wildcard = re.escape(wildcard)
211 wildcard = re.sub('^\\\\{', '(', wildcard)
212 wildcard = re.sub('\\\\,', '|', wildcard)
213 wildcard = re.sub('\\\\}$', ')', wildcard)
214 branch_re += wildcard
215 if glob_match.group(3):
216 branch_re += re.escape(glob_match.group(3))
217 match = re.match(branch_re, url)
218 if match:
219 return re.sub('\*$', match.group(1), as_ref)
220
221 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
222 if fetch_suburl:
223 full_url = base_url + '/' + fetch_suburl
224 else:
225 full_url = base_url
226 if full_url == url:
227 return as_ref
228 return None
229
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000230
iannucci@chromium.org79540052012-10-19 23:15:26 +0000231def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000232 """Prints statistics about the change to the user."""
233 # --no-ext-diff is broken in some versions of Git, so try to work around
234 # this by overriding the environment (but there is still a problem if the
235 # git config key "diff.external" is used).
236 env = os.environ.copy()
237 if 'GIT_EXTERNAL_DIFF' in env:
238 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000239 # 'cat' is a magical git string that disables pagers on all platforms.
240 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241
242 if find_copies:
243 similarity_options = ['--find-copies-harder', '-l100000',
244 '-C%s' % similarity]
245 else:
246 similarity_options = ['-M%s' % similarity]
247
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000249 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000250 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252
253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000254class Settings(object):
255 def __init__(self):
256 self.default_server = None
257 self.cc = None
258 self.root = None
259 self.is_git_svn = None
260 self.svn_branch = None
261 self.tree_status_url = None
262 self.viewvc_url = None
263 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000264 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000265 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266
267 def LazyUpdateIfNeeded(self):
268 """Updates the settings from a codereview.settings file, if available."""
269 if not self.updated:
270 cr_settings_file = FindCodereviewSettingsFile()
271 if cr_settings_file:
272 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000273 self.updated = True
274 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 self.updated = True
276
277 def GetDefaultServerUrl(self, error_ok=False):
278 if not self.default_server:
279 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000280 self.default_server = gclient_utils.UpgradeToHttps(
281 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282 if error_ok:
283 return self.default_server
284 if not self.default_server:
285 error_message = ('Could not find settings file. You must configure '
286 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000287 self.default_server = gclient_utils.UpgradeToHttps(
288 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289 return self.default_server
290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000291 def GetRoot(self):
292 if not self.root:
293 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
294 return self.root
295
296 def GetIsGitSvn(self):
297 """Return true if this repo looks like it's using git-svn."""
298 if self.is_git_svn is None:
299 # If you have any "svn-remote.*" config keys, we think you're using svn.
300 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000301 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 return self.is_git_svn
303
304 def GetSVNBranch(self):
305 if self.svn_branch is None:
306 if not self.GetIsGitSvn():
307 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
308
309 # Try to figure out which remote branch we're based on.
310 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000311 # 1) iterate through our branch history and find the svn URL.
312 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000313
314 # regexp matching the git-svn line that contains the URL.
315 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
316
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000317 env = os.environ.copy()
318 # 'cat' is a magical git string that disables pagers on all platforms.
319 env['GIT_PAGER'] = 'cat'
320
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000321 # We don't want to go through all of history, so read a line from the
322 # pipe at a time.
323 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000324 cmd = ['git', 'log', '-100', '--pretty=medium']
325 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000326 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000327 for line in proc.stdout:
328 match = git_svn_re.match(line)
329 if match:
330 url = match.group(1)
331 proc.stdout.close() # Cut pipe.
332 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000334 if url:
335 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
336 remotes = RunGit(['config', '--get-regexp',
337 r'^svn-remote\..*\.url']).splitlines()
338 for remote in remotes:
339 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000340 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000341 remote = match.group(1)
342 base_url = match.group(2)
343 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000344 ['config', 'svn-remote.%s.fetch' % remote],
345 error_ok=True).strip()
346 if fetch_spec:
347 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
348 if self.svn_branch:
349 break
350 branch_spec = RunGit(
351 ['config', 'svn-remote.%s.branches' % remote],
352 error_ok=True).strip()
353 if branch_spec:
354 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
355 if self.svn_branch:
356 break
357 tag_spec = RunGit(
358 ['config', 'svn-remote.%s.tags' % remote],
359 error_ok=True).strip()
360 if tag_spec:
361 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
362 if self.svn_branch:
363 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000364
365 if not self.svn_branch:
366 DieWithError('Can\'t guess svn branch -- try specifying it on the '
367 'command line')
368
369 return self.svn_branch
370
371 def GetTreeStatusUrl(self, error_ok=False):
372 if not self.tree_status_url:
373 error_message = ('You must configure your tree status URL by running '
374 '"git cl config".')
375 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
376 error_ok=error_ok,
377 error_message=error_message)
378 return self.tree_status_url
379
380 def GetViewVCUrl(self):
381 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000382 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000383 return self.viewvc_url
384
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000385 def GetDefaultCCList(self):
386 return self._GetConfig('rietveld.cc', error_ok=True)
387
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000388 def GetDefaultPrivateFlag(self):
389 return self._GetConfig('rietveld.private', error_ok=True)
390
ukai@chromium.orge8077812012-02-03 03:41:46 +0000391 def GetIsGerrit(self):
392 """Return true if this repo is assosiated with gerrit code review system."""
393 if self.is_gerrit is None:
394 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
395 return self.is_gerrit
396
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000397 def GetGitEditor(self):
398 """Return the editor specified in the git config, or None if none is."""
399 if self.git_editor is None:
400 self.git_editor = self._GetConfig('core.editor', error_ok=True)
401 return self.git_editor or None
402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403 def _GetConfig(self, param, **kwargs):
404 self.LazyUpdateIfNeeded()
405 return RunGit(['config', param], **kwargs).strip()
406
407
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000408def ShortBranchName(branch):
409 """Convert a name like 'refs/heads/foo' to just 'foo'."""
410 return branch.replace('refs/heads/', '')
411
412
413class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000414 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000416 global settings
417 if not settings:
418 # Happens when git_cl.py is used as a utility library.
419 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000420 settings.GetDefaultServerUrl()
421 self.branchref = branchref
422 if self.branchref:
423 self.branch = ShortBranchName(self.branchref)
424 else:
425 self.branch = None
426 self.rietveld_server = None
427 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000428 self.lookedup_issue = False
429 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 self.has_description = False
431 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000432 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000434 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000435 self.cc = None
436 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000437 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000438 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000439
440 def GetCCList(self):
441 """Return the users cc'd on this CL.
442
443 Return is a string suitable for passing to gcl with the --cc flag.
444 """
445 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000446 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000447 more_cc = ','.join(self.watchers)
448 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
449 return self.cc
450
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000451 def GetCCListWithoutDefault(self):
452 """Return the users cc'd on this CL excluding default ones."""
453 if self.cc is None:
454 self.cc = ','.join(self.watchers)
455 return self.cc
456
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000457 def SetWatchers(self, watchers):
458 """Set the list of email addresses that should be cc'd based on the changed
459 files in this CL.
460 """
461 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000462
463 def GetBranch(self):
464 """Returns the short branch name, e.g. 'master'."""
465 if not self.branch:
466 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
467 self.branch = ShortBranchName(self.branchref)
468 return self.branch
469
470 def GetBranchRef(self):
471 """Returns the full branch name, e.g. 'refs/heads/master'."""
472 self.GetBranch() # Poke the lazy loader.
473 return self.branchref
474
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000475 @staticmethod
476 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000477 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 e.g. 'origin', 'refs/heads/master'
479 """
480 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
482 error_ok=True).strip()
483 if upstream_branch:
484 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
485 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000486 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
487 error_ok=True).strip()
488 if upstream_branch:
489 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000490 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000491 # Fall back on trying a git-svn upstream branch.
492 if settings.GetIsGitSvn():
493 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000495 # Else, try to guess the origin remote.
496 remote_branches = RunGit(['branch', '-r']).split()
497 if 'origin/master' in remote_branches:
498 # Fall back on origin/master if it exits.
499 remote = 'origin'
500 upstream_branch = 'refs/heads/master'
501 elif 'origin/trunk' in remote_branches:
502 # Fall back on origin/trunk if it exists. Generally a shared
503 # git-svn clone
504 remote = 'origin'
505 upstream_branch = 'refs/heads/trunk'
506 else:
507 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000508Either pass complete "git diff"-style arguments, like
509 git cl upload origin/master
510or verify this branch is set up to track another (via the --track argument to
511"git checkout -b ...").""")
512
513 return remote, upstream_branch
514
515 def GetUpstreamBranch(self):
516 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000517 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000518 if remote is not '.':
519 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
520 self.upstream_branch = upstream_branch
521 return self.upstream_branch
522
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000523 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000524 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000525 remote, branch = None, self.GetBranch()
526 seen_branches = set()
527 while branch not in seen_branches:
528 seen_branches.add(branch)
529 remote, branch = self.FetchUpstreamTuple(branch)
530 branch = ShortBranchName(branch)
531 if remote != '.' or branch.startswith('refs/remotes'):
532 break
533 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000534 remotes = RunGit(['remote'], error_ok=True).split()
535 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000537 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000538 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000539 logging.warning('Could not determine which remote this change is '
540 'associated with, so defaulting to "%s". This may '
541 'not be what you want. You may prevent this message '
542 'by running "git svn info" as documented here: %s',
543 self._remote,
544 GIT_INSTRUCTIONS_URL)
545 else:
546 logging.warn('Could not determine which remote this change is '
547 'associated with. You may prevent this message by '
548 'running "git svn info" as documented here: %s',
549 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000550 branch = 'HEAD'
551 if branch.startswith('refs/remotes'):
552 self._remote = (remote, branch)
553 else:
554 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000555 return self._remote
556
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000557 def GitSanityChecks(self, upstream_git_obj):
558 """Checks git repo status and ensures diff is from local commits."""
559
560 # Verify the commit we're diffing against is in our current branch.
561 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
562 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
563 if upstream_sha != common_ancestor:
564 print >> sys.stderr, (
565 'ERROR: %s is not in the current branch. You may need to rebase '
566 'your tracking branch' % upstream_sha)
567 return False
568
569 # List the commits inside the diff, and verify they are all local.
570 commits_in_diff = RunGit(
571 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
572 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
573 remote_branch = remote_branch.strip()
574 if code != 0:
575 _, remote_branch = self.GetRemoteBranch()
576
577 commits_in_remote = RunGit(
578 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
579
580 common_commits = set(commits_in_diff) & set(commits_in_remote)
581 if common_commits:
582 print >> sys.stderr, (
583 'ERROR: Your diff contains %d commits already in %s.\n'
584 'Run "git log --oneline %s..HEAD" to get a list of commits in '
585 'the diff. If you are using a custom git flow, you can override'
586 ' the reference used for this check with "git config '
587 'gitcl.remotebranch <git-ref>".' % (
588 len(common_commits), remote_branch, upstream_git_obj))
589 return False
590 return True
591
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000592 def GetGitBaseUrlFromConfig(self):
593 """Return the configured base URL from branch.<branchname>.baseurl.
594
595 Returns None if it is not set.
596 """
597 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
598 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 def GetRemoteUrl(self):
601 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
602
603 Returns None if there is no remote.
604 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000605 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
607
608 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000609 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000610 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000612 self.issue = int(issue) or None if issue else None
613 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 return self.issue
615
616 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000617 if not self.rietveld_server:
618 # If we're on a branch then get the server potentially associated
619 # with that branch.
620 if self.GetIssue():
621 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
622 ['config', self._RietveldServer()], error_ok=True).strip())
623 if not self.rietveld_server:
624 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 return self.rietveld_server
626
627 def GetIssueURL(self):
628 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000629 if not self.GetIssue():
630 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
632
633 def GetDescription(self, pretty=False):
634 if not self.has_description:
635 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000636 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000637 try:
638 self.description = self.RpcServer().get_description(issue).strip()
639 except urllib2.HTTPError, e:
640 if e.code == 404:
641 DieWithError(
642 ('\nWhile fetching the description for issue %d, received a '
643 '404 (not found)\n'
644 'error. It is likely that you deleted this '
645 'issue on the server. If this is the\n'
646 'case, please run\n\n'
647 ' git cl issue 0\n\n'
648 'to clear the association with the deleted issue. Then run '
649 'this command again.') % issue)
650 else:
651 DieWithError(
652 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653 self.has_description = True
654 if pretty:
655 wrapper = textwrap.TextWrapper()
656 wrapper.initial_indent = wrapper.subsequent_indent = ' '
657 return wrapper.fill(self.description)
658 return self.description
659
660 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000661 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000662 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000663 patchset = RunGit(['config', self._PatchsetSetting()],
664 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000665 self.patchset = int(patchset) or None if patchset else None
666 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667 return self.patchset
668
669 def SetPatchset(self, patchset):
670 """Set this branch's patchset. If patchset=0, clears the patchset."""
671 if patchset:
672 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000673 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674 else:
675 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000676 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000677 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000679 def GetMostRecentPatchset(self):
680 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000681
682 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000683 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000684 '/download/issue%s_%s.diff' % (issue, patchset))
685
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000686 def GetIssueProperties(self):
687 if self._props is None:
688 issue = self.GetIssue()
689 if not issue:
690 self._props = {}
691 else:
692 self._props = self.RpcServer().get_issue_properties(issue, True)
693 return self._props
694
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000695 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000696 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698 def SetIssue(self, issue):
699 """Set this branch's issue. If issue=0, clears the issue."""
700 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000701 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702 RunGit(['config', self._IssueSetting(), str(issue)])
703 if self.rietveld_server:
704 RunGit(['config', self._RietveldServer(), self.rietveld_server])
705 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000706 current_issue = self.GetIssue()
707 if current_issue:
708 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000709 self.issue = None
710 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000712 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000713 if not self.GitSanityChecks(upstream_branch):
714 DieWithError('\nGit sanity check failure')
715
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000716 env = os.environ.copy()
717 # 'cat' is a magical git string that disables pagers on all platforms.
718 env['GIT_PAGER'] = 'cat'
719
720 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000721 if not root:
722 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000723 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000724
725 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000726 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000727 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000728 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000729 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000730 except subprocess2.CalledProcessError:
731 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000732 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000733 'This branch probably doesn\'t exist anymore. To reset the\n'
734 'tracking branch, please run\n'
735 ' git branch --set-upstream %s trunk\n'
736 'replacing trunk with origin/master or the relevant branch') %
737 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000738
maruel@chromium.org52424302012-08-29 15:14:30 +0000739 issue = self.GetIssue()
740 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000741 if issue:
742 description = self.GetDescription()
743 else:
744 # If the change was never uploaded, use the log messages of all commits
745 # up to the branch point, as git cl upload will prefill the description
746 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000747 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000748 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000749 '%s...' % (upstream_branch)],
750 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000751
752 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000753 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000754 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000755 name,
756 description,
757 absroot,
758 files,
759 issue,
760 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000761 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000762
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000763 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000764 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000765
766 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000767 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000768 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000769 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000770 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000771 except presubmit_support.PresubmitFailure, e:
772 DieWithError(
773 ('%s\nMaybe your depot_tools is out of date?\n'
774 'If all fails, contact maruel@') % e)
775
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000776 def UpdateDescription(self, description):
777 self.description = description
778 return self.RpcServer().update_description(
779 self.GetIssue(), self.description)
780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000782 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000783 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000785 def SetFlag(self, flag, value):
786 """Patchset must match."""
787 if not self.GetPatchset():
788 DieWithError('The patchset needs to match. Send another patchset.')
789 try:
790 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000791 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000792 except urllib2.HTTPError, e:
793 if e.code == 404:
794 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
795 if e.code == 403:
796 DieWithError(
797 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
798 'match?') % (self.GetIssue(), self.GetPatchset()))
799 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000801 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 """Returns an upload.RpcServer() to access this review's rietveld instance.
803 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000804 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000805 self._rpc_server = rietveld.CachingRietveld(
806 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000807 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808
809 def _IssueSetting(self):
810 """Return the git setting that stores this change's issue."""
811 return 'branch.%s.rietveldissue' % self.GetBranch()
812
813 def _PatchsetSetting(self):
814 """Return the git setting that stores this change's most recent patchset."""
815 return 'branch.%s.rietveldpatchset' % self.GetBranch()
816
817 def _RietveldServer(self):
818 """Returns the git setting that stores this change's rietveld server."""
819 return 'branch.%s.rietveldserver' % self.GetBranch()
820
821
822def GetCodereviewSettingsInteractively():
823 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000824 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 server = settings.GetDefaultServerUrl(error_ok=True)
826 prompt = 'Rietveld server (host[:port])'
827 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000828 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829 if not server and not newserver:
830 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000831 if newserver:
832 newserver = gclient_utils.UpgradeToHttps(newserver)
833 if newserver != server:
834 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000836 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 prompt = caption
838 if initial:
839 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000840 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 if new_val == 'x':
842 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000843 elif new_val:
844 if is_url:
845 new_val = gclient_utils.UpgradeToHttps(new_val)
846 if new_val != initial:
847 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000849 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000850 SetProperty(settings.GetDefaultPrivateFlag(),
851 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000853 'tree-status-url', False)
854 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855
856 # TODO: configure a default branch to diff against, rather than this
857 # svn-based hackery.
858
859
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000860class ChangeDescription(object):
861 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000862 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000863 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000864
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000865 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000866 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000867
agable@chromium.org42c20792013-09-12 17:34:49 +0000868 @property # www.logilab.org/ticket/89786
869 def description(self): # pylint: disable=E0202
870 return '\n'.join(self._description_lines)
871
872 def set_description(self, desc):
873 if isinstance(desc, basestring):
874 lines = desc.splitlines()
875 else:
876 lines = [line.rstrip() for line in desc]
877 while lines and not lines[0]:
878 lines.pop(0)
879 while lines and not lines[-1]:
880 lines.pop(-1)
881 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882
883 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000884 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000885 assert isinstance(reviewers, list), reviewers
886 if not reviewers:
887 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000888 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000889
agable@chromium.org42c20792013-09-12 17:34:49 +0000890 # Get the set of R= and TBR= lines and remove them from the desciption.
891 regexp = re.compile(self.R_LINE)
892 matches = [regexp.match(line) for line in self._description_lines]
893 new_desc = [l for i, l in enumerate(self._description_lines)
894 if not matches[i]]
895 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000896
agable@chromium.org42c20792013-09-12 17:34:49 +0000897 # Construct new unified R= and TBR= lines.
898 r_names = []
899 tbr_names = []
900 for match in matches:
901 if not match:
902 continue
903 people = cleanup_list([match.group(2).strip()])
904 if match.group(1) == 'TBR':
905 tbr_names.extend(people)
906 else:
907 r_names.extend(people)
908 for name in r_names:
909 if name not in reviewers:
910 reviewers.append(name)
911 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
912 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
913
914 # Put the new lines in the description where the old first R= line was.
915 line_loc = next((i for i, match in enumerate(matches) if match), -1)
916 if 0 <= line_loc < len(self._description_lines):
917 if new_tbr_line:
918 self._description_lines.insert(line_loc, new_tbr_line)
919 if new_r_line:
920 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000921 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000922 if new_r_line:
923 self.append_footer(new_r_line)
924 if new_tbr_line:
925 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000926
927 def prompt(self):
928 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000929 self.set_description([
930 '# Enter a description of the change.',
931 '# This will be displayed on the codereview site.',
932 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000933 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000934 '--------------------',
935 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000936
agable@chromium.org42c20792013-09-12 17:34:49 +0000937 regexp = re.compile(self.BUG_LINE)
938 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000939 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000940 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000941 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000942 if not content:
943 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000944 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000945
946 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
948 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000949 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000950 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000951
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000952 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000953 if self._description_lines:
954 # Add an empty line if either the last line or the new line isn't a tag.
955 last_line = self._description_lines[-1]
956 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
957 not presubmit_support.Change.TAG_LINE_RE.match(line)):
958 self._description_lines.append('')
959 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000960
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000961 def get_reviewers(self):
962 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
964 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000965 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000966
967
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000968def get_approving_reviewers(props):
969 """Retrieves the reviewers that approved a CL from the issue properties with
970 messages.
971
972 Note that the list may contain reviewers that are not committer, thus are not
973 considered by the CQ.
974 """
975 return sorted(
976 set(
977 message['sender']
978 for message in props['messages']
979 if message['approval'] and message['sender'] in props['reviewers']
980 )
981 )
982
983
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984def FindCodereviewSettingsFile(filename='codereview.settings'):
985 """Finds the given file starting in the cwd and going up.
986
987 Only looks up to the top of the repository unless an
988 'inherit-review-settings-ok' file exists in the root of the repository.
989 """
990 inherit_ok_file = 'inherit-review-settings-ok'
991 cwd = os.getcwd()
992 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
993 if os.path.isfile(os.path.join(root, inherit_ok_file)):
994 root = '/'
995 while True:
996 if filename in os.listdir(cwd):
997 if os.path.isfile(os.path.join(cwd, filename)):
998 return open(os.path.join(cwd, filename))
999 if cwd == root:
1000 break
1001 cwd = os.path.dirname(cwd)
1002
1003
1004def LoadCodereviewSettingsFromFile(fileobj):
1005 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001006 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 def SetProperty(name, setting, unset_error_ok=False):
1009 fullname = 'rietveld.' + name
1010 if setting in keyvals:
1011 RunGit(['config', fullname, keyvals[setting]])
1012 else:
1013 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1014
1015 SetProperty('server', 'CODE_REVIEW_SERVER')
1016 # Only server setting is required. Other settings can be absent.
1017 # In that case, we ignore errors raised during option deletion attempt.
1018 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001019 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1021 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1022
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001023 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001024 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1027 #should be of the form
1028 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1029 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1030 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1031 keyvals['ORIGIN_URL_CONFIG']])
1032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001034def urlretrieve(source, destination):
1035 """urllib is broken for SSL connections via a proxy therefore we
1036 can't use urllib.urlretrieve()."""
1037 with open(destination, 'w') as f:
1038 f.write(urllib2.urlopen(source).read())
1039
1040
ukai@chromium.org712d6102013-11-27 00:52:58 +00001041def hasSheBang(fname):
1042 """Checks fname is a #! script."""
1043 with open(fname) as f:
1044 return f.read(2).startswith('#!')
1045
1046
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001047def DownloadHooks(force):
1048 """downloads hooks
1049
1050 Args:
1051 force: True to update hooks. False to install hooks if not present.
1052 """
1053 if not settings.GetIsGerrit():
1054 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001055 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001056 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1057 if not os.access(dst, os.X_OK):
1058 if os.path.exists(dst):
1059 if not force:
1060 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001061 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001062 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001063 if not hasSheBang(dst):
1064 DieWithError('Not a script: %s\n'
1065 'You need to download from\n%s\n'
1066 'into .git/hooks/commit-msg and '
1067 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001068 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1069 except Exception:
1070 if os.path.exists(dst):
1071 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001072 DieWithError('\nFailed to download hooks.\n'
1073 'You need to download from\n%s\n'
1074 'into .git/hooks/commit-msg and '
1075 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001076
1077
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001078@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001079def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001080 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001082 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 if len(args) == 0:
1084 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001085 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086 return 0
1087
1088 url = args[0]
1089 if not url.endswith('codereview.settings'):
1090 url = os.path.join(url, 'codereview.settings')
1091
1092 # Load code review settings and download hooks (if available).
1093 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001094 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 return 0
1096
1097
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001098def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001099 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001100 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1101 branch = ShortBranchName(branchref)
1102 _, args = parser.parse_args(args)
1103 if not args:
1104 print("Current base-url:")
1105 return RunGit(['config', 'branch.%s.base-url' % branch],
1106 error_ok=False).strip()
1107 else:
1108 print("Setting base-url to %s" % args[0])
1109 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1110 error_ok=False).strip()
1111
1112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001114 """Show status of changelists.
1115
1116 Colors are used to tell the state of the CL unless --fast is used:
1117 - Green LGTM'ed
1118 - Blue waiting for review
1119 - Yellow waiting for you to reply to review
1120 - Red not sent for review or broken
1121 - Cyan was committed, branch can be deleted
1122
1123 Also see 'git cl comments'.
1124 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 parser.add_option('--field',
1126 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001127 parser.add_option('-f', '--fast', action='store_true',
1128 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001130 if args:
1131 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001134 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 if options.field.startswith('desc'):
1136 print cl.GetDescription()
1137 elif options.field == 'id':
1138 issueid = cl.GetIssue()
1139 if issueid:
1140 print issueid
1141 elif options.field == 'patch':
1142 patchset = cl.GetPatchset()
1143 if patchset:
1144 print patchset
1145 elif options.field == 'url':
1146 url = cl.GetIssueURL()
1147 if url:
1148 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001149 return 0
1150
1151 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1152 if not branches:
1153 print('No local branch found.')
1154 return 0
1155
1156 changes = (Changelist(branchref=b) for b in branches.splitlines())
1157 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1158 alignment = max(5, max(len(b) for b in branches))
1159 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 # Adhoc thread pool to request data concurrently.
1161 output = Queue.Queue()
1162
1163 # Silence upload.py otherwise it becomes unweldly.
1164 upload.verbosity = 0
1165
1166 if not options.fast:
1167 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001168 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001169 c = Changelist(branchref=b)
1170 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001171 props = {}
1172 r = None
1173 if i:
1174 try:
1175 props = c.GetIssueProperties()
1176 r = c.GetApprovingReviewers() if i else None
1177 except urllib2.HTTPError:
1178 # The issue probably doesn't exist anymore.
1179 i += ' (broken)'
1180
1181 msgs = props.get('messages') or []
1182
1183 if not i:
1184 color = Fore.WHITE
1185 elif props.get('closed'):
1186 # Issue is closed.
1187 color = Fore.CYAN
1188 elif r:
1189 # Was LGTM'ed.
1190 color = Fore.GREEN
1191 elif not msgs:
1192 # No message was sent.
1193 color = Fore.RED
1194 elif msgs[-1]['sender'] != props.get('owner_email'):
1195 color = Fore.YELLOW
1196 else:
1197 color = Fore.BLUE
1198 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001199
1200 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1201 for t in threads:
1202 t.daemon = True
1203 t.start()
1204 else:
1205 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1206 for b in branches:
1207 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001208 url = c.GetIssueURL()
1209 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001210
1211 tmp = {}
1212 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001213 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001214 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001215 b, i, color = output.get()
1216 tmp[b] = (i, color)
1217 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001218 reset = Fore.RESET
1219 if not sys.stdout.isatty():
1220 color = ''
1221 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001222 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001223 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001225 cl = Changelist()
1226 print
1227 print 'Current branch:',
1228 if not cl.GetIssue():
1229 print 'no issue assigned.'
1230 return 0
1231 print cl.GetBranch()
1232 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1233 print 'Issue description:'
1234 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235 return 0
1236
1237
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001238def colorize_CMDstatus_doc():
1239 """To be called once in main() to add colors to git cl status help."""
1240 colors = [i for i in dir(Fore) if i[0].isupper()]
1241
1242 def colorize_line(line):
1243 for color in colors:
1244 if color in line.upper():
1245 # Extract whitespaces first and the leading '-'.
1246 indent = len(line) - len(line.lstrip(' ')) + 1
1247 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1248 return line
1249
1250 lines = CMDstatus.__doc__.splitlines()
1251 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1252
1253
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001254@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001256 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257
1258 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001259 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001260 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261
1262 cl = Changelist()
1263 if len(args) > 0:
1264 try:
1265 issue = int(args[0])
1266 except ValueError:
1267 DieWithError('Pass a number to set the issue or none to list it.\n'
1268 'Maybe you want to run git cl status?')
1269 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001270 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 return 0
1272
1273
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001274def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001275 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001276 (_, args) = parser.parse_args(args)
1277 if args:
1278 parser.error('Unsupported argument: %s' % args)
1279
1280 cl = Changelist()
1281 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001283 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001284 if message['disapproval']:
1285 color = Fore.RED
1286 elif message['approval']:
1287 color = Fore.GREEN
1288 elif message['sender'] == data['owner_email']:
1289 color = Fore.MAGENTA
1290 else:
1291 color = Fore.BLUE
1292 print '\n%s%s %s%s' % (
1293 color, message['date'].split('.', 1)[0], message['sender'],
1294 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001295 if message['text'].strip():
1296 print '\n'.join(' ' + l for l in message['text'].splitlines())
1297 return 0
1298
1299
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001300def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001301 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001302 cl = Changelist()
1303 if not cl.GetIssue():
1304 DieWithError('This branch has no associated changelist.')
1305 description = ChangeDescription(cl.GetDescription())
1306 description.prompt()
1307 cl.UpdateDescription(description.description)
1308 return 0
1309
1310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311def CreateDescriptionFromLog(args):
1312 """Pulls out the commit log to use as a base for the CL description."""
1313 log_args = []
1314 if len(args) == 1 and not args[0].endswith('.'):
1315 log_args = [args[0] + '..']
1316 elif len(args) == 1 and args[0].endswith('...'):
1317 log_args = [args[0][:-1]]
1318 elif len(args) == 2:
1319 log_args = [args[0] + '..' + args[1]]
1320 else:
1321 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001322 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323
1324
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001326 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001327 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001329 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001330 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 (options, args) = parser.parse_args(args)
1332
ukai@chromium.org259e4682012-10-25 07:36:33 +00001333 if not options.force and is_dirty_git_tree('presubmit'):
1334 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335 return 1
1336
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001337 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 if args:
1339 base_branch = args[0]
1340 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001341 # Default to diffing against the common ancestor of the upstream branch.
1342 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001344 cl.RunHook(
1345 committing=not options.upload,
1346 may_prompt=False,
1347 verbose=options.verbose,
1348 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001349 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
1351
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001352def AddChangeIdToCommitMessage(options, args):
1353 """Re-commits using the current message, assumes the commit hook is in
1354 place.
1355 """
1356 log_desc = options.message or CreateDescriptionFromLog(args)
1357 git_command = ['commit', '--amend', '-m', log_desc]
1358 RunGit(git_command)
1359 new_log_desc = CreateDescriptionFromLog(args)
1360 if CHANGE_ID in new_log_desc:
1361 print 'git-cl: Added Change-Id to commit message.'
1362 else:
1363 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1364
1365
ukai@chromium.orge8077812012-02-03 03:41:46 +00001366def GerritUpload(options, args, cl):
1367 """upload the current branch to gerrit."""
1368 # We assume the remote called "origin" is the one we want.
1369 # It is probably not worthwhile to support different workflows.
1370 remote = 'origin'
1371 branch = 'master'
1372 if options.target_branch:
1373 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001375 change_desc = ChangeDescription(
1376 options.message or CreateDescriptionFromLog(args))
1377 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001378 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001380 if CHANGE_ID not in change_desc.description:
1381 AddChangeIdToCommitMessage(options, args)
1382 if options.reviewers:
1383 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385 receive_options = []
1386 cc = cl.GetCCList().split(',')
1387 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001388 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001389 cc = filter(None, cc)
1390 if cc:
1391 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001392 if change_desc.get_reviewers():
1393 receive_options.extend(
1394 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
ukai@chromium.orge8077812012-02-03 03:41:46 +00001396 git_command = ['push']
1397 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001398 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001399 ' '.join(receive_options))
1400 git_command += [remote, 'HEAD:refs/for/' + branch]
1401 RunGit(git_command)
1402 # TODO(ukai): parse Change-Id: and set issue number?
1403 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405
ukai@chromium.orge8077812012-02-03 03:41:46 +00001406def RietveldUpload(options, args, cl):
1407 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1409 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410 if options.emulate_svn_auto_props:
1411 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412
1413 change_desc = None
1414
1415 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001416 if options.title:
1417 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001418 if options.message:
1419 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001420 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421 print ("This branch is associated with issue %s. "
1422 "Adding patch to that issue." % cl.GetIssue())
1423 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001424 if options.title:
1425 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001426 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001427 change_desc = ChangeDescription(message)
1428 if options.reviewers:
1429 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001430 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001431 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001432
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001433 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 print "Description is empty; aborting."
1435 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001436
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001437 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001438 if change_desc.get_reviewers():
1439 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001440 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001441 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001442 DieWithError("Must specify reviewers to send email.")
1443 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001444
1445 # We check this before applying rietveld.private assuming that in
1446 # rietveld.cc only addresses which we can send private CLs to are listed
1447 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1448 # --private is specified explicitly on the command line.
1449 if options.private:
1450 logging.warn('rietveld.cc is ignored since private flag is specified. '
1451 'You need to review and add them manually if necessary.')
1452 cc = cl.GetCCListWithoutDefault()
1453 else:
1454 cc = cl.GetCCList()
1455 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001456 if cc:
1457 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001459 if options.private or settings.GetDefaultPrivateFlag() == "True":
1460 upload_args.append('--private')
1461
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001462 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001463 if not options.find_copies:
1464 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001465
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 # Include the upstream repo's URL in the change -- this is useful for
1467 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001468 remote_url = cl.GetGitBaseUrlFromConfig()
1469 if not remote_url:
1470 if settings.GetIsGitSvn():
1471 # URL is dependent on the current directory.
1472 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1473 if data:
1474 keys = dict(line.split(': ', 1) for line in data.splitlines()
1475 if ': ' in line)
1476 remote_url = keys.get('URL', None)
1477 else:
1478 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1479 remote_url = (cl.GetRemoteUrl() + '@'
1480 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481 if remote_url:
1482 upload_args.extend(['--base_url', remote_url])
1483
1484 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001485 upload_args = ['upload'] + upload_args + args
1486 logging.info('upload.RealMain(%s)', upload_args)
1487 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001488 issue = int(issue)
1489 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001490 except KeyboardInterrupt:
1491 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 except:
1493 # If we got an exception after the user typed a description for their
1494 # change, back up the description before re-raising.
1495 if change_desc:
1496 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1497 print '\nGot exception while uploading -- saving description to %s\n' \
1498 % backup_path
1499 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001500 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 backup_file.close()
1502 raise
1503
1504 if not cl.GetIssue():
1505 cl.SetIssue(issue)
1506 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001507
1508 if options.use_commit_queue:
1509 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510 return 0
1511
1512
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001513def cleanup_list(l):
1514 """Fixes a list so that comma separated items are put as individual items.
1515
1516 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1517 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1518 """
1519 items = sum((i.split(',') for i in l), [])
1520 stripped_items = (i.strip() for i in items)
1521 return sorted(filter(None, stripped_items))
1522
1523
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001524@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001525def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001526 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001527 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1528 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001529 parser.add_option('--bypass-watchlists', action='store_true',
1530 dest='bypass_watchlists',
1531 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001532 parser.add_option('-f', action='store_true', dest='force',
1533 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001534 parser.add_option('-m', dest='message', help='message for patchset')
1535 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001536 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001537 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001538 help='reviewer email addresses')
1539 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001540 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001542 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001543 help='send email to reviewer immediately')
1544 parser.add_option("--emulate_svn_auto_props", action="store_true",
1545 dest="emulate_svn_auto_props",
1546 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547 parser.add_option('-c', '--use-commit-queue', action='store_true',
1548 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001549 parser.add_option('--private', action='store_true',
1550 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001551 parser.add_option('--target_branch',
1552 help='When uploading to gerrit, remote branch to '
1553 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001554 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001555 (options, args) = parser.parse_args(args)
1556
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001557 if options.target_branch and not settings.GetIsGerrit():
1558 parser.error('Use --target_branch for non gerrit repository.')
1559
ukai@chromium.org259e4682012-10-25 07:36:33 +00001560 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001561 return 1
1562
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001563 options.reviewers = cleanup_list(options.reviewers)
1564 options.cc = cleanup_list(options.cc)
1565
ukai@chromium.orge8077812012-02-03 03:41:46 +00001566 cl = Changelist()
1567 if args:
1568 # TODO(ukai): is it ok for gerrit case?
1569 base_branch = args[0]
1570 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001571 # Default to diffing against common ancestor of upstream branch
1572 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001573 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001574
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001575 # Apply watchlists on upload.
1576 change = cl.GetChange(base_branch, None)
1577 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1578 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001579 if not options.bypass_watchlists:
1580 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001581
ukai@chromium.orge8077812012-02-03 03:41:46 +00001582 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001583 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001584 may_prompt=not options.force,
1585 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001586 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001587 if not hook_results.should_continue():
1588 return 1
1589 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001590 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001591
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001592 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001593 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001594 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001595 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001596 print ('The last upload made from this repository was patchset #%d but '
1597 'the most recent patchset on the server is #%d.'
1598 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001599 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1600 'from another machine or branch the patch you\'re uploading now '
1601 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001602 ask_for_data('About to upload; enter to confirm.')
1603
iannucci@chromium.org79540052012-10-19 23:15:26 +00001604 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001605 if settings.GetIsGerrit():
1606 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001607 ret = RietveldUpload(options, args, cl)
1608 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001609 git_set_branch_value('last-upload-hash',
1610 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001611
1612 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001613
1614
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001615def IsSubmoduleMergeCommit(ref):
1616 # When submodules are added to the repo, we expect there to be a single
1617 # non-git-svn merge commit at remote HEAD with a signature comment.
1618 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001619 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001620 return RunGit(cmd) != ''
1621
1622
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623def SendUpstream(parser, args, cmd):
1624 """Common code for CmdPush and CmdDCommit
1625
1626 Squashed commit into a single.
1627 Updates changelog with metadata (e.g. pointer to review).
1628 Pushes/dcommits the code upstream.
1629 Updates review and closes.
1630 """
1631 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1632 help='bypass upload presubmit hook')
1633 parser.add_option('-m', dest='message',
1634 help="override review description")
1635 parser.add_option('-f', action='store_true', dest='force',
1636 help="force yes to questions (don't prompt)")
1637 parser.add_option('-c', dest='contributor',
1638 help="external contributor for patch (appended to " +
1639 "description and used as author for git). Should be " +
1640 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001641 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 (options, args) = parser.parse_args(args)
1643 cl = Changelist()
1644
1645 if not args or cmd == 'push':
1646 # Default to merging against our best guess of the upstream branch.
1647 args = [cl.GetUpstreamBranch()]
1648
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001649 if options.contributor:
1650 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1651 print "Please provide contibutor as 'First Last <email@example.com>'"
1652 return 1
1653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001655 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656
ukai@chromium.org259e4682012-10-25 07:36:33 +00001657 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001658 return 1
1659
1660 # This rev-list syntax means "show all commits not in my branch that
1661 # are in base_branch".
1662 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1663 base_branch]).splitlines()
1664 if upstream_commits:
1665 print ('Base branch "%s" has %d commits '
1666 'not in this branch.' % (base_branch, len(upstream_commits)))
1667 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1668 return 1
1669
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001670 # This is the revision `svn dcommit` will commit on top of.
1671 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1672 '--pretty=format:%H'])
1673
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001675 # If the base_head is a submodule merge commit, the first parent of the
1676 # base_head should be a git-svn commit, which is what we're interested in.
1677 base_svn_head = base_branch
1678 if base_has_submodules:
1679 base_svn_head += '^1'
1680
1681 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 if extra_commits:
1683 print ('This branch has %d additional commits not upstreamed yet.'
1684 % len(extra_commits.splitlines()))
1685 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1686 'before attempting to %s.' % (base_branch, cmd))
1687 return 1
1688
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001689 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001690 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001691 author = None
1692 if options.contributor:
1693 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001694 hook_results = cl.RunHook(
1695 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001696 may_prompt=not options.force,
1697 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001698 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001699 if not hook_results.should_continue():
1700 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701
1702 if cmd == 'dcommit':
1703 # Check the tree status if the tree status URL is set.
1704 status = GetTreeStatus()
1705 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001706 print('The tree is closed. Please wait for it to reopen. Use '
1707 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708 return 1
1709 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001710 print('Unable to determine tree status. Please verify manually and '
1711 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001712 else:
1713 breakpad.SendStack(
1714 'GitClHooksBypassedCommit',
1715 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001716 (cl.GetRietveldServer(), cl.GetIssue()),
1717 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001719 change_desc = ChangeDescription(options.message)
1720 if not change_desc.description and cl.GetIssue():
1721 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001723 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001724 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001725 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001726 else:
1727 print 'No description set.'
1728 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1729 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001731 # Keep a separate copy for the commit message, because the commit message
1732 # contains the link to the Rietveld issue, while the Rietveld message contains
1733 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001734 # Keep a separate copy for the commit message.
1735 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001736 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001737
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001738 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001739 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001740 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001741 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001742 commit_desc.append_footer('Patch from %s.' % options.contributor)
1743
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001744 print('Description:')
1745 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746
1747 branches = [base_branch, cl.GetBranchRef()]
1748 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001749 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001750 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001752 # We want to squash all this branch's commits into one commit with the proper
1753 # description. We do this by doing a "reset --soft" to the base branch (which
1754 # keeps the working copy the same), then dcommitting that. If origin/master
1755 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1756 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001757 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001758 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1759 # Delete the branches if they exist.
1760 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1761 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1762 result = RunGitWithCode(showref_cmd)
1763 if result[0] == 0:
1764 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765
1766 # We might be in a directory that's present in this branch but not in the
1767 # trunk. Move up to the top of the tree so that git commands that expect a
1768 # valid CWD won't fail after we check out the merge branch.
1769 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1770 if rel_base_path:
1771 os.chdir(rel_base_path)
1772
1773 # Stuff our change into the merge branch.
1774 # We wrap in a try...finally block so if anything goes wrong,
1775 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001776 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001778 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1779 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001781 RunGit(
1782 [
1783 'commit', '--author', options.contributor,
1784 '-m', commit_desc.description,
1785 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001787 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001788 if base_has_submodules:
1789 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1790 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1791 RunGit(['checkout', CHERRY_PICK_BRANCH])
1792 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001793 if cmd == 'push':
1794 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001795 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 retcode, output = RunGitWithCode(
1797 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1798 logging.debug(output)
1799 else:
1800 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001801 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001802 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001803 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001804 finally:
1805 # And then swap back to the original branch and clean up.
1806 RunGit(['checkout', '-q', cl.GetBranch()])
1807 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001808 if base_has_submodules:
1809 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810
1811 if cl.GetIssue():
1812 if cmd == 'dcommit' and 'Committed r' in output:
1813 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1814 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001815 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1816 for l in output.splitlines(False))
1817 match = filter(None, match)
1818 if len(match) != 1:
1819 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1820 output)
1821 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001822 else:
1823 return 1
1824 viewvc_url = settings.GetViewVCUrl()
1825 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001826 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001827 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001828 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 print ('Closing issue '
1830 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001831 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001833 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001834 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001835 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001836 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1837 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001838 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001839
1840 if retcode == 0:
1841 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1842 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001843 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 return 0
1846
1847
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001848@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001850 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001852 message = """This doesn't appear to be an SVN repository.
1853If your project has a git mirror with an upstream SVN master, you probably need
1854to run 'git svn init', see your project's git mirror documentation.
1855If your project has a true writeable upstream repository, you probably want
1856to run 'git cl push' instead.
1857Choose wisely, if you get this wrong, your commit might appear to succeed but
1858will instead be silently ignored."""
1859 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001860 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861 return SendUpstream(parser, args, 'dcommit')
1862
1863
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001864@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001866 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867 if settings.GetIsGitSvn():
1868 print('This appears to be an SVN repository.')
1869 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001870 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 return SendUpstream(parser, args, 'push')
1872
1873
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001874@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001876 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001877 parser.add_option('-b', dest='newbranch',
1878 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001879 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001881 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1882 help='Change to the directory DIR immediately, '
1883 'before doing anything else.')
1884 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001885 help='failed patches spew .rej files rather than '
1886 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1888 help="don't commit after patch applies")
1889 (options, args) = parser.parse_args(args)
1890 if len(args) != 1:
1891 parser.print_help()
1892 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001893 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001895 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001896 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001897
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001898 if options.newbranch:
1899 if options.force:
1900 RunGit(['branch', '-D', options.newbranch],
1901 stderr=subprocess2.PIPE, error_ok=True)
1902 RunGit(['checkout', '-b', options.newbranch,
1903 Changelist().GetUpstreamBranch()])
1904
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001905 return PatchIssue(issue_arg, options.reject, options.nocommit,
1906 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001907
1908
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001909def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001910 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001912 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001913 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001914 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001915 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001916 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001917 # Assume it's a URL to the patch. Default to https.
1918 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001919 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001920 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921 DieWithError('Must pass an issue ID or full URL for '
1922 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001923 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001924 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001925 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 # Switch up to the top-level directory, if necessary, in preparation for
1928 # applying the patch.
1929 top = RunGit(['rev-parse', '--show-cdup']).strip()
1930 if top:
1931 os.chdir(top)
1932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 # Git patches have a/ at the beginning of source paths. We strip that out
1934 # with a sed script rather than the -p flag to patch so we can feed either
1935 # Git or svn-style patches into the same apply command.
1936 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001937 try:
1938 patch_data = subprocess2.check_output(
1939 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1940 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941 DieWithError('Git patch mungling failed.')
1942 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001943 env = os.environ.copy()
1944 # 'cat' is a magical git string that disables pagers on all platforms.
1945 env['GIT_PAGER'] = 'cat'
1946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947 # We use "git apply" to apply the patch instead of "patch" so that we can
1948 # pick up file adds.
1949 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001950 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001951 if directory:
1952 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001953 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001954 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001955 elif IsGitVersionAtLeast('1.7.12'):
1956 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001957 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001958 subprocess2.check_call(cmd, env=env,
1959 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001960 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001961 DieWithError('Failed to apply the patch')
1962
1963 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001964 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1966 cl = Changelist()
1967 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001968 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001969 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970 else:
1971 print "Patch applied to index."
1972 return 0
1973
1974
1975def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001976 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977 # Provide a wrapper for git svn rebase to help avoid accidental
1978 # git svn dcommit.
1979 # It's the only command that doesn't use parser at all since we just defer
1980 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001981 env = os.environ.copy()
1982 # 'cat' is a magical git string that disables pagers on all platforms.
1983 env['GIT_PAGER'] = 'cat'
1984
1985 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986
1987
1988def GetTreeStatus():
1989 """Fetches the tree status and returns either 'open', 'closed',
1990 'unknown' or 'unset'."""
1991 url = settings.GetTreeStatusUrl(error_ok=True)
1992 if url:
1993 status = urllib2.urlopen(url).read().lower()
1994 if status.find('closed') != -1 or status == '0':
1995 return 'closed'
1996 elif status.find('open') != -1 or status == '1':
1997 return 'open'
1998 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 return 'unset'
2000
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002def GetTreeStatusReason():
2003 """Fetches the tree status from a json url and returns the message
2004 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002005 url = settings.GetTreeStatusUrl()
2006 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007 connection = urllib2.urlopen(json_url)
2008 status = json.loads(connection.read())
2009 connection.close()
2010 return status['message']
2011
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002013def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002014 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002015 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016 status = GetTreeStatus()
2017 if 'unset' == status:
2018 print 'You must configure your tree status URL by running "git cl config".'
2019 return 2
2020
2021 print "The tree is %s" % status
2022 print
2023 print GetTreeStatusReason()
2024 if status != 'open':
2025 return 1
2026 return 0
2027
2028
maruel@chromium.org15192402012-09-06 12:38:29 +00002029def CMDtry(parser, args):
2030 """Triggers a try job through Rietveld."""
2031 group = optparse.OptionGroup(parser, "Try job options")
2032 group.add_option(
2033 "-b", "--bot", action="append",
2034 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2035 "times to specify multiple builders. ex: "
2036 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2037 "the try server waterfall for the builders name and the tests "
2038 "available. Can also be used to specify gtest_filter, e.g. "
2039 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2040 group.add_option(
2041 "-r", "--revision",
2042 help="Revision to use for the try job; default: the "
2043 "revision will be determined by the try server; see "
2044 "its waterfall for more info")
2045 group.add_option(
2046 "-c", "--clobber", action="store_true", default=False,
2047 help="Force a clobber before building; e.g. don't do an "
2048 "incremental build")
2049 group.add_option(
2050 "--project",
2051 help="Override which project to use. Projects are defined "
2052 "server-side to define what default bot set to use")
2053 group.add_option(
2054 "-t", "--testfilter", action="append", default=[],
2055 help=("Apply a testfilter to all the selected builders. Unless the "
2056 "builders configurations are similar, use multiple "
2057 "--bot <builder>:<test> arguments."))
2058 group.add_option(
2059 "-n", "--name", help="Try job name; default to current branch name")
2060 parser.add_option_group(group)
2061 options, args = parser.parse_args(args)
2062
2063 if args:
2064 parser.error('Unknown arguments: %s' % args)
2065
2066 cl = Changelist()
2067 if not cl.GetIssue():
2068 parser.error('Need to upload first')
2069
2070 if not options.name:
2071 options.name = cl.GetBranch()
2072
2073 # Process --bot and --testfilter.
2074 if not options.bot:
2075 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002076 change = cl.GetChange(
2077 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2078 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002079 options.bot = presubmit_support.DoGetTrySlaves(
2080 change,
2081 change.LocalPaths(),
2082 settings.GetRoot(),
2083 None,
2084 None,
2085 options.verbose,
2086 sys.stdout)
2087 if not options.bot:
2088 parser.error('No default try builder to try, use --bot')
2089
2090 builders_and_tests = {}
2091 for bot in options.bot:
2092 if ':' in bot:
2093 builder, tests = bot.split(':', 1)
2094 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2095 elif ',' in bot:
2096 parser.error('Specify one bot per --bot flag')
2097 else:
2098 builders_and_tests.setdefault(bot, []).append('defaulttests')
2099
2100 if options.testfilter:
2101 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2102 builders_and_tests = dict(
2103 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2104 if t != ['compile'])
2105
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002106 if any('triggered' in b for b in builders_and_tests):
2107 print >> sys.stderr, (
2108 'ERROR You are trying to send a job to a triggered bot. This type of'
2109 ' bot requires an\ninitial job from a parent (usually a builder). '
2110 'Instead send your job to the parent.\n'
2111 'Bot list: %s' % builders_and_tests)
2112 return 1
2113
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002114 patchset = cl.GetMostRecentPatchset()
2115 if patchset and patchset != cl.GetPatchset():
2116 print(
2117 '\nWARNING Mismatch between local config and server. Did a previous '
2118 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2119 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002120
2121 cl.RpcServer().trigger_try_jobs(
2122 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2123 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002124 print('Tried jobs on:')
2125 length = max(len(builder) for builder in builders_and_tests)
2126 for builder in sorted(builders_and_tests):
2127 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002128 return 0
2129
2130
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002131@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002132def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002133 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002134 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002135 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002136 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002139 if args:
2140 # One arg means set upstream branch.
2141 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2142 cl = Changelist()
2143 print "Upstream branch set to " + cl.GetUpstreamBranch()
2144 else:
2145 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002146 return 0
2147
2148
thestig@chromium.org00858c82013-12-02 23:08:03 +00002149def CMDweb(parser, args):
2150 """Opens the current CL in the web browser."""
2151 _, args = parser.parse_args(args)
2152 if args:
2153 parser.error('Unrecognized args: %s' % ' '.join(args))
2154
2155 issue_url = Changelist().GetIssueURL()
2156 if not issue_url:
2157 print >> sys.stderr, 'ERROR No issue to open'
2158 return 1
2159
2160 webbrowser.open(issue_url)
2161 return 0
2162
2163
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002164def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002165 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002166 _, args = parser.parse_args(args)
2167 if args:
2168 parser.error('Unrecognized args: %s' % ' '.join(args))
2169 cl = Changelist()
2170 cl.SetFlag('commit', '1')
2171 return 0
2172
2173
groby@chromium.org411034a2013-02-26 15:12:01 +00002174def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002175 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002176 _, args = parser.parse_args(args)
2177 if args:
2178 parser.error('Unrecognized args: %s' % ' '.join(args))
2179 cl = Changelist()
2180 # Ensure there actually is an issue to close.
2181 cl.GetDescription()
2182 cl.CloseIssue()
2183 return 0
2184
2185
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002186def CMDdiff(parser, args):
2187 """shows differences between local tree and last upload."""
2188 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002189 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002190 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002191 if not issue:
2192 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002193 TMP_BRANCH = 'git-cl-diff'
2194 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2195
2196 # Create a new branch based on the merge-base
2197 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2198 try:
2199 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002200 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002201 if rtn != 0:
2202 return rtn
2203
2204 # Switch back to starting brand and diff against the temporary
2205 # branch containing the latest rietveld patch.
2206 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2207 finally:
2208 RunGit(['checkout', '-q', branch])
2209 RunGit(['branch', '-D', TMP_BRANCH])
2210
2211 return 0
2212
2213
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002214def CMDowners(parser, args):
2215 """interactively find the owners for reviewing"""
2216 parser.add_option(
2217 '--no-color',
2218 action='store_true',
2219 help='Use this option to disable color output')
2220 options, args = parser.parse_args(args)
2221
2222 author = RunGit(['config', 'user.email']).strip() or None
2223
2224 cl = Changelist()
2225
2226 if args:
2227 if len(args) > 1:
2228 parser.error('Unknown args')
2229 base_branch = args[0]
2230 else:
2231 # Default to diffing against the common ancestor of the upstream branch.
2232 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2233
2234 change = cl.GetChange(base_branch, None)
2235 return owners_finder.OwnersFinder(
2236 [f.LocalPath() for f in
2237 cl.GetChange(base_branch, None).AffectedFiles()],
2238 change.RepositoryRoot(), author,
2239 fopen=file, os_path=os.path, glob=glob.glob,
2240 disable_color=options.no_color).run()
2241
2242
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002243def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002244 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002245 CLANG_EXTS = ['.cc', '.cpp', '.h']
2246 parser.add_option('--full', action='store_true', default=False)
2247 opts, args = parser.parse_args(args)
2248 if args:
2249 parser.error('Unrecognized args: %s' % ' '.join(args))
2250
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002251 # git diff generates paths against the root of the repository. Change
2252 # to that directory so clang-format can find files even within subdirs.
2253 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2254 if rel_base_path:
2255 os.chdir(rel_base_path)
2256
digit@chromium.org29e47272013-05-17 17:01:46 +00002257 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002258 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002259 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002260 # Only list the names of modified files.
2261 diff_cmd.append('--name-only')
2262 else:
2263 # Only generate context-less patches.
2264 diff_cmd.append('-U0')
2265
2266 # Grab the merge-base commit, i.e. the upstream commit of the current
2267 # branch when it was created or the last time it was rebased. This is
2268 # to cover the case where the user may have called "git fetch origin",
2269 # moving the origin branch to a newer commit, but hasn't rebased yet.
2270 upstream_commit = None
2271 cl = Changelist()
2272 upstream_branch = cl.GetUpstreamBranch()
2273 if upstream_branch:
2274 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2275 upstream_commit = upstream_commit.strip()
2276
2277 if not upstream_commit:
2278 DieWithError('Could not find base commit for this branch. '
2279 'Are you in detached state?')
2280
2281 diff_cmd.append(upstream_commit)
2282
2283 # Handle source file filtering.
2284 diff_cmd.append('--')
2285 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2286 diff_output = RunGit(diff_cmd)
2287
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002288 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2289
digit@chromium.org29e47272013-05-17 17:01:46 +00002290 if opts.full:
2291 # diff_output is a list of files to send to clang-format.
2292 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002293 if not files:
2294 print "Nothing to format."
2295 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002296 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2297 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002298 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002299 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002300 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2301 'clang-format-diff.py')
2302 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002303 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002304 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002305
2306 # Newer versions of clang-format-diff.py require an explicit -i flag
2307 # to apply the edits to files, otherwise it just displays a diff.
2308 # Probe the usage string to verify if this is needed.
2309 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2310 if '[-i]' in help_text:
2311 cmd.append('-i')
2312
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002313 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002314
2315 return 0
2316
2317
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002318class OptionParser(optparse.OptionParser):
2319 """Creates the option parse and add --verbose support."""
2320 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002321 optparse.OptionParser.__init__(
2322 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002323 self.add_option(
2324 '-v', '--verbose', action='count', default=0,
2325 help='Use 2 times for more debugging info')
2326
2327 def parse_args(self, args=None, values=None):
2328 options, args = optparse.OptionParser.parse_args(self, args, values)
2329 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2330 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2331 return options, args
2332
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002335 if sys.hexversion < 0x02060000:
2336 print >> sys.stderr, (
2337 '\nYour python version %s is unsupported, please upgrade.\n' %
2338 sys.version.split(' ', 1)[0])
2339 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002340
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002341 # Reload settings.
2342 global settings
2343 settings = Settings()
2344
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002345 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002346 dispatcher = subcommand.CommandDispatcher(__name__)
2347 try:
2348 return dispatcher.execute(OptionParser(), argv)
2349 except urllib2.HTTPError, e:
2350 if e.code != 500:
2351 raise
2352 DieWithError(
2353 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2354 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002355
2356
2357if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002358 # These affect sys.stdout so do it outside of main() to simplify mocks in
2359 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002360 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002361 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002362 sys.exit(main(sys.argv[1:]))