blob: 9d7cb4e519e2a4c73ab80d9cb82ca049434812a4 [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:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000270 # The only value that actually changes the behavior is
271 # autoupdate = "false". Everything else means "true".
272 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
273 error_ok=True
274 ).strip().lower()
275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000277 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000279 # set updated to True to avoid infinite calling loop
280 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000281 self.updated = True
282 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000283 self.updated = True
284
285 def GetDefaultServerUrl(self, error_ok=False):
286 if not self.default_server:
287 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000288 self.default_server = gclient_utils.UpgradeToHttps(
289 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 if error_ok:
291 return self.default_server
292 if not self.default_server:
293 error_message = ('Could not find settings file. You must configure '
294 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000295 self.default_server = gclient_utils.UpgradeToHttps(
296 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 return self.default_server
298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299 def GetRoot(self):
300 if not self.root:
301 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
302 return self.root
303
304 def GetIsGitSvn(self):
305 """Return true if this repo looks like it's using git-svn."""
306 if self.is_git_svn is None:
307 # If you have any "svn-remote.*" config keys, we think you're using svn.
308 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000309 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 return self.is_git_svn
311
312 def GetSVNBranch(self):
313 if self.svn_branch is None:
314 if not self.GetIsGitSvn():
315 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
316
317 # Try to figure out which remote branch we're based on.
318 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000319 # 1) iterate through our branch history and find the svn URL.
320 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321
322 # regexp matching the git-svn line that contains the URL.
323 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
324
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000325 env = os.environ.copy()
326 # 'cat' is a magical git string that disables pagers on all platforms.
327 env['GIT_PAGER'] = 'cat'
328
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000329 # We don't want to go through all of history, so read a line from the
330 # pipe at a time.
331 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000332 cmd = ['git', 'log', '-100', '--pretty=medium']
333 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000334 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000335 for line in proc.stdout:
336 match = git_svn_re.match(line)
337 if match:
338 url = match.group(1)
339 proc.stdout.close() # Cut pipe.
340 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000341
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000342 if url:
343 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
344 remotes = RunGit(['config', '--get-regexp',
345 r'^svn-remote\..*\.url']).splitlines()
346 for remote in remotes:
347 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000349 remote = match.group(1)
350 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000351 rewrite_root = RunGit(
352 ['config', 'svn-remote.%s.rewriteRoot' % remote],
353 error_ok=True).strip()
354 if rewrite_root:
355 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000356 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000357 ['config', 'svn-remote.%s.fetch' % remote],
358 error_ok=True).strip()
359 if fetch_spec:
360 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
361 if self.svn_branch:
362 break
363 branch_spec = RunGit(
364 ['config', 'svn-remote.%s.branches' % remote],
365 error_ok=True).strip()
366 if branch_spec:
367 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
368 if self.svn_branch:
369 break
370 tag_spec = RunGit(
371 ['config', 'svn-remote.%s.tags' % remote],
372 error_ok=True).strip()
373 if tag_spec:
374 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
375 if self.svn_branch:
376 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000377
378 if not self.svn_branch:
379 DieWithError('Can\'t guess svn branch -- try specifying it on the '
380 'command line')
381
382 return self.svn_branch
383
384 def GetTreeStatusUrl(self, error_ok=False):
385 if not self.tree_status_url:
386 error_message = ('You must configure your tree status URL by running '
387 '"git cl config".')
388 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
389 error_ok=error_ok,
390 error_message=error_message)
391 return self.tree_status_url
392
393 def GetViewVCUrl(self):
394 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000395 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000396 return self.viewvc_url
397
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000398 def GetDefaultCCList(self):
399 return self._GetConfig('rietveld.cc', error_ok=True)
400
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000401 def GetDefaultPrivateFlag(self):
402 return self._GetConfig('rietveld.private', error_ok=True)
403
ukai@chromium.orge8077812012-02-03 03:41:46 +0000404 def GetIsGerrit(self):
405 """Return true if this repo is assosiated with gerrit code review system."""
406 if self.is_gerrit is None:
407 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
408 return self.is_gerrit
409
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000410 def GetGitEditor(self):
411 """Return the editor specified in the git config, or None if none is."""
412 if self.git_editor is None:
413 self.git_editor = self._GetConfig('core.editor', error_ok=True)
414 return self.git_editor or None
415
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000416 def _GetConfig(self, param, **kwargs):
417 self.LazyUpdateIfNeeded()
418 return RunGit(['config', param], **kwargs).strip()
419
420
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000421def ShortBranchName(branch):
422 """Convert a name like 'refs/heads/foo' to just 'foo'."""
423 return branch.replace('refs/heads/', '')
424
425
426class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000427 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000429 global settings
430 if not settings:
431 # Happens when git_cl.py is used as a utility library.
432 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433 settings.GetDefaultServerUrl()
434 self.branchref = branchref
435 if self.branchref:
436 self.branch = ShortBranchName(self.branchref)
437 else:
438 self.branch = None
439 self.rietveld_server = None
440 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000441 self.lookedup_issue = False
442 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000443 self.has_description = False
444 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000445 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000447 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000448 self.cc = None
449 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000450 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000451 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000452
453 def GetCCList(self):
454 """Return the users cc'd on this CL.
455
456 Return is a string suitable for passing to gcl with the --cc flag.
457 """
458 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000459 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000460 more_cc = ','.join(self.watchers)
461 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
462 return self.cc
463
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000464 def GetCCListWithoutDefault(self):
465 """Return the users cc'd on this CL excluding default ones."""
466 if self.cc is None:
467 self.cc = ','.join(self.watchers)
468 return self.cc
469
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000470 def SetWatchers(self, watchers):
471 """Set the list of email addresses that should be cc'd based on the changed
472 files in this CL.
473 """
474 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000475
476 def GetBranch(self):
477 """Returns the short branch name, e.g. 'master'."""
478 if not self.branch:
479 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
480 self.branch = ShortBranchName(self.branchref)
481 return self.branch
482
483 def GetBranchRef(self):
484 """Returns the full branch name, e.g. 'refs/heads/master'."""
485 self.GetBranch() # Poke the lazy loader.
486 return self.branchref
487
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000488 @staticmethod
489 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000490 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 e.g. 'origin', 'refs/heads/master'
492 """
493 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
495 error_ok=True).strip()
496 if upstream_branch:
497 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
498 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000499 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
500 error_ok=True).strip()
501 if upstream_branch:
502 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000504 # Fall back on trying a git-svn upstream branch.
505 if settings.GetIsGitSvn():
506 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000508 # Else, try to guess the origin remote.
509 remote_branches = RunGit(['branch', '-r']).split()
510 if 'origin/master' in remote_branches:
511 # Fall back on origin/master if it exits.
512 remote = 'origin'
513 upstream_branch = 'refs/heads/master'
514 elif 'origin/trunk' in remote_branches:
515 # Fall back on origin/trunk if it exists. Generally a shared
516 # git-svn clone
517 remote = 'origin'
518 upstream_branch = 'refs/heads/trunk'
519 else:
520 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000521Either pass complete "git diff"-style arguments, like
522 git cl upload origin/master
523or verify this branch is set up to track another (via the --track argument to
524"git checkout -b ...").""")
525
526 return remote, upstream_branch
527
528 def GetUpstreamBranch(self):
529 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000530 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000531 if remote is not '.':
532 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
533 self.upstream_branch = upstream_branch
534 return self.upstream_branch
535
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000537 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000538 remote, branch = None, self.GetBranch()
539 seen_branches = set()
540 while branch not in seen_branches:
541 seen_branches.add(branch)
542 remote, branch = self.FetchUpstreamTuple(branch)
543 branch = ShortBranchName(branch)
544 if remote != '.' or branch.startswith('refs/remotes'):
545 break
546 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000547 remotes = RunGit(['remote'], error_ok=True).split()
548 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000549 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000550 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000551 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000552 logging.warning('Could not determine which remote this change is '
553 'associated with, so defaulting to "%s". This may '
554 'not be what you want. You may prevent this message '
555 'by running "git svn info" as documented here: %s',
556 self._remote,
557 GIT_INSTRUCTIONS_URL)
558 else:
559 logging.warn('Could not determine which remote this change is '
560 'associated with. You may prevent this message by '
561 'running "git svn info" as documented here: %s',
562 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000563 branch = 'HEAD'
564 if branch.startswith('refs/remotes'):
565 self._remote = (remote, branch)
566 else:
567 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000568 return self._remote
569
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000570 def GitSanityChecks(self, upstream_git_obj):
571 """Checks git repo status and ensures diff is from local commits."""
572
573 # Verify the commit we're diffing against is in our current branch.
574 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
575 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
576 if upstream_sha != common_ancestor:
577 print >> sys.stderr, (
578 'ERROR: %s is not in the current branch. You may need to rebase '
579 'your tracking branch' % upstream_sha)
580 return False
581
582 # List the commits inside the diff, and verify they are all local.
583 commits_in_diff = RunGit(
584 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
585 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
586 remote_branch = remote_branch.strip()
587 if code != 0:
588 _, remote_branch = self.GetRemoteBranch()
589
590 commits_in_remote = RunGit(
591 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
592
593 common_commits = set(commits_in_diff) & set(commits_in_remote)
594 if common_commits:
595 print >> sys.stderr, (
596 'ERROR: Your diff contains %d commits already in %s.\n'
597 'Run "git log --oneline %s..HEAD" to get a list of commits in '
598 'the diff. If you are using a custom git flow, you can override'
599 ' the reference used for this check with "git config '
600 'gitcl.remotebranch <git-ref>".' % (
601 len(common_commits), remote_branch, upstream_git_obj))
602 return False
603 return True
604
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000605 def GetGitBaseUrlFromConfig(self):
606 """Return the configured base URL from branch.<branchname>.baseurl.
607
608 Returns None if it is not set.
609 """
610 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
611 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000612
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 def GetRemoteUrl(self):
614 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
615
616 Returns None if there is no remote.
617 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000618 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
620
621 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000622 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000623 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000625 self.issue = int(issue) or None if issue else None
626 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 return self.issue
628
629 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000630 if not self.rietveld_server:
631 # If we're on a branch then get the server potentially associated
632 # with that branch.
633 if self.GetIssue():
634 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
635 ['config', self._RietveldServer()], error_ok=True).strip())
636 if not self.rietveld_server:
637 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 return self.rietveld_server
639
640 def GetIssueURL(self):
641 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000642 if not self.GetIssue():
643 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
645
646 def GetDescription(self, pretty=False):
647 if not self.has_description:
648 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000649 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000650 try:
651 self.description = self.RpcServer().get_description(issue).strip()
652 except urllib2.HTTPError, e:
653 if e.code == 404:
654 DieWithError(
655 ('\nWhile fetching the description for issue %d, received a '
656 '404 (not found)\n'
657 'error. It is likely that you deleted this '
658 'issue on the server. If this is the\n'
659 'case, please run\n\n'
660 ' git cl issue 0\n\n'
661 'to clear the association with the deleted issue. Then run '
662 'this command again.') % issue)
663 else:
664 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000665 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666 self.has_description = True
667 if pretty:
668 wrapper = textwrap.TextWrapper()
669 wrapper.initial_indent = wrapper.subsequent_indent = ' '
670 return wrapper.fill(self.description)
671 return self.description
672
673 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000674 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000675 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 patchset = RunGit(['config', self._PatchsetSetting()],
677 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000678 self.patchset = int(patchset) or None if patchset else None
679 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 return self.patchset
681
682 def SetPatchset(self, patchset):
683 """Set this branch's patchset. If patchset=0, clears the patchset."""
684 if patchset:
685 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000686 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687 else:
688 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000689 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000690 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000692 def GetMostRecentPatchset(self):
693 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000694
695 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000696 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000697 '/download/issue%s_%s.diff' % (issue, patchset))
698
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000699 def GetIssueProperties(self):
700 if self._props is None:
701 issue = self.GetIssue()
702 if not issue:
703 self._props = {}
704 else:
705 self._props = self.RpcServer().get_issue_properties(issue, True)
706 return self._props
707
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000708 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000709 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711 def SetIssue(self, issue):
712 """Set this branch's issue. If issue=0, clears the issue."""
713 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000714 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 RunGit(['config', self._IssueSetting(), str(issue)])
716 if self.rietveld_server:
717 RunGit(['config', self._RietveldServer(), self.rietveld_server])
718 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000719 current_issue = self.GetIssue()
720 if current_issue:
721 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000722 self.issue = None
723 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000725 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000726 if not self.GitSanityChecks(upstream_branch):
727 DieWithError('\nGit sanity check failure')
728
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000729 env = os.environ.copy()
730 # 'cat' is a magical git string that disables pagers on all platforms.
731 env['GIT_PAGER'] = 'cat'
732
733 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000734 if not root:
735 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000736 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000737
738 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000739 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000740 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000741 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000742 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000743 except subprocess2.CalledProcessError:
744 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000745 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000746 'This branch probably doesn\'t exist anymore. To reset the\n'
747 'tracking branch, please run\n'
748 ' git branch --set-upstream %s trunk\n'
749 'replacing trunk with origin/master or the relevant branch') %
750 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000751
maruel@chromium.org52424302012-08-29 15:14:30 +0000752 issue = self.GetIssue()
753 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000754 if issue:
755 description = self.GetDescription()
756 else:
757 # If the change was never uploaded, use the log messages of all commits
758 # up to the branch point, as git cl upload will prefill the description
759 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000760 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000761 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000762 '%s...' % (upstream_branch)],
763 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000764
765 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000766 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000767 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000768 name,
769 description,
770 absroot,
771 files,
772 issue,
773 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000774 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000775
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000776 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000777 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000778
779 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000780 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000781 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000782 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000783 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000784 except presubmit_support.PresubmitFailure, e:
785 DieWithError(
786 ('%s\nMaybe your depot_tools is out of date?\n'
787 'If all fails, contact maruel@') % e)
788
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000789 def UpdateDescription(self, description):
790 self.description = description
791 return self.RpcServer().update_description(
792 self.GetIssue(), self.description)
793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000795 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000796 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000798 def SetFlag(self, flag, value):
799 """Patchset must match."""
800 if not self.GetPatchset():
801 DieWithError('The patchset needs to match. Send another patchset.')
802 try:
803 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000804 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000805 except urllib2.HTTPError, e:
806 if e.code == 404:
807 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
808 if e.code == 403:
809 DieWithError(
810 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
811 'match?') % (self.GetIssue(), self.GetPatchset()))
812 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000814 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 """Returns an upload.RpcServer() to access this review's rietveld instance.
816 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000817 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000818 self._rpc_server = rietveld.CachingRietveld(
819 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000820 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821
822 def _IssueSetting(self):
823 """Return the git setting that stores this change's issue."""
824 return 'branch.%s.rietveldissue' % self.GetBranch()
825
826 def _PatchsetSetting(self):
827 """Return the git setting that stores this change's most recent patchset."""
828 return 'branch.%s.rietveldpatchset' % self.GetBranch()
829
830 def _RietveldServer(self):
831 """Returns the git setting that stores this change's rietveld server."""
832 return 'branch.%s.rietveldserver' % self.GetBranch()
833
834
835def GetCodereviewSettingsInteractively():
836 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000837 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 server = settings.GetDefaultServerUrl(error_ok=True)
839 prompt = 'Rietveld server (host[:port])'
840 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000841 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 if not server and not newserver:
843 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000844 if newserver:
845 newserver = gclient_utils.UpgradeToHttps(newserver)
846 if newserver != server:
847 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000849 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850 prompt = caption
851 if initial:
852 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000853 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 if new_val == 'x':
855 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000856 elif new_val:
857 if is_url:
858 new_val = gclient_utils.UpgradeToHttps(new_val)
859 if new_val != initial:
860 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000862 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000863 SetProperty(settings.GetDefaultPrivateFlag(),
864 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000866 'tree-status-url', False)
867 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868
869 # TODO: configure a default branch to diff against, rather than this
870 # svn-based hackery.
871
872
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000873class ChangeDescription(object):
874 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000875 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000876 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000877
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000878 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000879 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880
agable@chromium.org42c20792013-09-12 17:34:49 +0000881 @property # www.logilab.org/ticket/89786
882 def description(self): # pylint: disable=E0202
883 return '\n'.join(self._description_lines)
884
885 def set_description(self, desc):
886 if isinstance(desc, basestring):
887 lines = desc.splitlines()
888 else:
889 lines = [line.rstrip() for line in desc]
890 while lines and not lines[0]:
891 lines.pop(0)
892 while lines and not lines[-1]:
893 lines.pop(-1)
894 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000895
896 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000897 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000898 assert isinstance(reviewers, list), reviewers
899 if not reviewers:
900 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000901 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000902
agable@chromium.org42c20792013-09-12 17:34:49 +0000903 # Get the set of R= and TBR= lines and remove them from the desciption.
904 regexp = re.compile(self.R_LINE)
905 matches = [regexp.match(line) for line in self._description_lines]
906 new_desc = [l for i, l in enumerate(self._description_lines)
907 if not matches[i]]
908 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000909
agable@chromium.org42c20792013-09-12 17:34:49 +0000910 # Construct new unified R= and TBR= lines.
911 r_names = []
912 tbr_names = []
913 for match in matches:
914 if not match:
915 continue
916 people = cleanup_list([match.group(2).strip()])
917 if match.group(1) == 'TBR':
918 tbr_names.extend(people)
919 else:
920 r_names.extend(people)
921 for name in r_names:
922 if name not in reviewers:
923 reviewers.append(name)
924 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
925 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
926
927 # Put the new lines in the description where the old first R= line was.
928 line_loc = next((i for i, match in enumerate(matches) if match), -1)
929 if 0 <= line_loc < len(self._description_lines):
930 if new_tbr_line:
931 self._description_lines.insert(line_loc, new_tbr_line)
932 if new_r_line:
933 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000934 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000935 if new_r_line:
936 self.append_footer(new_r_line)
937 if new_tbr_line:
938 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000939
940 def prompt(self):
941 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 self.set_description([
943 '# Enter a description of the change.',
944 '# This will be displayed on the codereview site.',
945 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000946 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 '--------------------',
948 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000949
agable@chromium.org42c20792013-09-12 17:34:49 +0000950 regexp = re.compile(self.BUG_LINE)
951 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000952 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000953 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000954 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000955 if not content:
956 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000957 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000958
959 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000960 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
961 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000962 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000964
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000965 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000966 if self._description_lines:
967 # Add an empty line if either the last line or the new line isn't a tag.
968 last_line = self._description_lines[-1]
969 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
970 not presubmit_support.Change.TAG_LINE_RE.match(line)):
971 self._description_lines.append('')
972 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000973
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000974 def get_reviewers(self):
975 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000976 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
977 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000978 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000979
980
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000981def get_approving_reviewers(props):
982 """Retrieves the reviewers that approved a CL from the issue properties with
983 messages.
984
985 Note that the list may contain reviewers that are not committer, thus are not
986 considered by the CQ.
987 """
988 return sorted(
989 set(
990 message['sender']
991 for message in props['messages']
992 if message['approval'] and message['sender'] in props['reviewers']
993 )
994 )
995
996
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997def FindCodereviewSettingsFile(filename='codereview.settings'):
998 """Finds the given file starting in the cwd and going up.
999
1000 Only looks up to the top of the repository unless an
1001 'inherit-review-settings-ok' file exists in the root of the repository.
1002 """
1003 inherit_ok_file = 'inherit-review-settings-ok'
1004 cwd = os.getcwd()
1005 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
1006 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1007 root = '/'
1008 while True:
1009 if filename in os.listdir(cwd):
1010 if os.path.isfile(os.path.join(cwd, filename)):
1011 return open(os.path.join(cwd, filename))
1012 if cwd == root:
1013 break
1014 cwd = os.path.dirname(cwd)
1015
1016
1017def LoadCodereviewSettingsFromFile(fileobj):
1018 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001019 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021 def SetProperty(name, setting, unset_error_ok=False):
1022 fullname = 'rietveld.' + name
1023 if setting in keyvals:
1024 RunGit(['config', fullname, keyvals[setting]])
1025 else:
1026 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1027
1028 SetProperty('server', 'CODE_REVIEW_SERVER')
1029 # Only server setting is required. Other settings can be absent.
1030 # In that case, we ignore errors raised during option deletion attempt.
1031 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001032 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1034 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1035
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001036 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001037 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1040 #should be of the form
1041 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1042 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1043 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1044 keyvals['ORIGIN_URL_CONFIG']])
1045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001047def urlretrieve(source, destination):
1048 """urllib is broken for SSL connections via a proxy therefore we
1049 can't use urllib.urlretrieve()."""
1050 with open(destination, 'w') as f:
1051 f.write(urllib2.urlopen(source).read())
1052
1053
ukai@chromium.org712d6102013-11-27 00:52:58 +00001054def hasSheBang(fname):
1055 """Checks fname is a #! script."""
1056 with open(fname) as f:
1057 return f.read(2).startswith('#!')
1058
1059
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001060def DownloadHooks(force):
1061 """downloads hooks
1062
1063 Args:
1064 force: True to update hooks. False to install hooks if not present.
1065 """
1066 if not settings.GetIsGerrit():
1067 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001068 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001069 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1070 if not os.access(dst, os.X_OK):
1071 if os.path.exists(dst):
1072 if not force:
1073 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001074 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001075 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001076 if not hasSheBang(dst):
1077 DieWithError('Not a script: %s\n'
1078 'You need to download from\n%s\n'
1079 'into .git/hooks/commit-msg and '
1080 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001081 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1082 except Exception:
1083 if os.path.exists(dst):
1084 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001085 DieWithError('\nFailed to download hooks.\n'
1086 'You need to download from\n%s\n'
1087 'into .git/hooks/commit-msg and '
1088 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001089
1090
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001091@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001093 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001095 parser.add_option('--activate-update', action='store_true',
1096 help='activate auto-updating [rietveld] section in '
1097 '.git/config')
1098 parser.add_option('--deactivate-update', action='store_true',
1099 help='deactivate auto-updating [rietveld] section in '
1100 '.git/config')
1101 options, args = parser.parse_args(args)
1102
1103 if options.deactivate_update:
1104 RunGit(['config', 'rietveld.autoupdate', 'false'])
1105 return
1106
1107 if options.activate_update:
1108 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1109 return
1110
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 if len(args) == 0:
1112 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001113 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114 return 0
1115
1116 url = args[0]
1117 if not url.endswith('codereview.settings'):
1118 url = os.path.join(url, 'codereview.settings')
1119
1120 # Load code review settings and download hooks (if available).
1121 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001122 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 return 0
1124
1125
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001126def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001127 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001128 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1129 branch = ShortBranchName(branchref)
1130 _, args = parser.parse_args(args)
1131 if not args:
1132 print("Current base-url:")
1133 return RunGit(['config', 'branch.%s.base-url' % branch],
1134 error_ok=False).strip()
1135 else:
1136 print("Setting base-url to %s" % args[0])
1137 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1138 error_ok=False).strip()
1139
1140
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001142 """Show status of changelists.
1143
1144 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001145 - Red not sent for review or broken
1146 - Blue waiting for review
1147 - Yellow waiting for you to reply to review
1148 - Green LGTM'ed
1149 - Magenta in the commit queue
1150 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001151
1152 Also see 'git cl comments'.
1153 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 parser.add_option('--field',
1155 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001156 parser.add_option('-f', '--fast', action='store_true',
1157 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001159 if args:
1160 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001163 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 if options.field.startswith('desc'):
1165 print cl.GetDescription()
1166 elif options.field == 'id':
1167 issueid = cl.GetIssue()
1168 if issueid:
1169 print issueid
1170 elif options.field == 'patch':
1171 patchset = cl.GetPatchset()
1172 if patchset:
1173 print patchset
1174 elif options.field == 'url':
1175 url = cl.GetIssueURL()
1176 if url:
1177 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001178 return 0
1179
1180 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1181 if not branches:
1182 print('No local branch found.')
1183 return 0
1184
1185 changes = (Changelist(branchref=b) for b in branches.splitlines())
1186 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1187 alignment = max(5, max(len(b) for b in branches))
1188 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001189 # Adhoc thread pool to request data concurrently.
1190 output = Queue.Queue()
1191
1192 # Silence upload.py otherwise it becomes unweldly.
1193 upload.verbosity = 0
1194
1195 if not options.fast:
1196 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001197 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001198 c = Changelist(branchref=b)
1199 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001200 props = {}
1201 r = None
1202 if i:
1203 try:
1204 props = c.GetIssueProperties()
1205 r = c.GetApprovingReviewers() if i else None
1206 except urllib2.HTTPError:
1207 # The issue probably doesn't exist anymore.
1208 i += ' (broken)'
1209
1210 msgs = props.get('messages') or []
1211
1212 if not i:
1213 color = Fore.WHITE
1214 elif props.get('closed'):
1215 # Issue is closed.
1216 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001217 elif props.get('commit'):
1218 # Issue is in the commit queue.
1219 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001220 elif r:
1221 # Was LGTM'ed.
1222 color = Fore.GREEN
1223 elif not msgs:
1224 # No message was sent.
1225 color = Fore.RED
1226 elif msgs[-1]['sender'] != props.get('owner_email'):
1227 color = Fore.YELLOW
1228 else:
1229 color = Fore.BLUE
1230 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001231
1232 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1233 for t in threads:
1234 t.daemon = True
1235 t.start()
1236 else:
1237 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1238 for b in branches:
1239 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001240 url = c.GetIssueURL()
1241 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001242
1243 tmp = {}
1244 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001245 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001246 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001247 b, i, color = output.get()
1248 tmp[b] = (i, color)
1249 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001250 reset = Fore.RESET
1251 if not sys.stdout.isatty():
1252 color = ''
1253 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001254 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001255 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001256
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001257 cl = Changelist()
1258 print
1259 print 'Current branch:',
1260 if not cl.GetIssue():
1261 print 'no issue assigned.'
1262 return 0
1263 print cl.GetBranch()
1264 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1265 print 'Issue description:'
1266 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 return 0
1268
1269
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001270def colorize_CMDstatus_doc():
1271 """To be called once in main() to add colors to git cl status help."""
1272 colors = [i for i in dir(Fore) if i[0].isupper()]
1273
1274 def colorize_line(line):
1275 for color in colors:
1276 if color in line.upper():
1277 # Extract whitespaces first and the leading '-'.
1278 indent = len(line) - len(line.lstrip(' ')) + 1
1279 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1280 return line
1281
1282 lines = CMDstatus.__doc__.splitlines()
1283 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1284
1285
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001286@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001288 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289
1290 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001291 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001292 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293
1294 cl = Changelist()
1295 if len(args) > 0:
1296 try:
1297 issue = int(args[0])
1298 except ValueError:
1299 DieWithError('Pass a number to set the issue or none to list it.\n'
1300 'Maybe you want to run git cl status?')
1301 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001302 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 return 0
1304
1305
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001306def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001307 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001308 (_, args) = parser.parse_args(args)
1309 if args:
1310 parser.error('Unsupported argument: %s' % args)
1311
1312 cl = Changelist()
1313 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001314 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001315 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001316 if message['disapproval']:
1317 color = Fore.RED
1318 elif message['approval']:
1319 color = Fore.GREEN
1320 elif message['sender'] == data['owner_email']:
1321 color = Fore.MAGENTA
1322 else:
1323 color = Fore.BLUE
1324 print '\n%s%s %s%s' % (
1325 color, message['date'].split('.', 1)[0], message['sender'],
1326 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001327 if message['text'].strip():
1328 print '\n'.join(' ' + l for l in message['text'].splitlines())
1329 return 0
1330
1331
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001332def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001333 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001334 cl = Changelist()
1335 if not cl.GetIssue():
1336 DieWithError('This branch has no associated changelist.')
1337 description = ChangeDescription(cl.GetDescription())
1338 description.prompt()
1339 cl.UpdateDescription(description.description)
1340 return 0
1341
1342
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343def CreateDescriptionFromLog(args):
1344 """Pulls out the commit log to use as a base for the CL description."""
1345 log_args = []
1346 if len(args) == 1 and not args[0].endswith('.'):
1347 log_args = [args[0] + '..']
1348 elif len(args) == 1 and args[0].endswith('...'):
1349 log_args = [args[0][:-1]]
1350 elif len(args) == 2:
1351 log_args = [args[0] + '..' + args[1]]
1352 else:
1353 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001354 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355
1356
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001358 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001359 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001361 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001362 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363 (options, args) = parser.parse_args(args)
1364
ukai@chromium.org259e4682012-10-25 07:36:33 +00001365 if not options.force and is_dirty_git_tree('presubmit'):
1366 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 return 1
1368
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001369 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370 if args:
1371 base_branch = args[0]
1372 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001373 # Default to diffing against the common ancestor of the upstream branch.
1374 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001376 cl.RunHook(
1377 committing=not options.upload,
1378 may_prompt=False,
1379 verbose=options.verbose,
1380 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001381 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382
1383
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001384def AddChangeIdToCommitMessage(options, args):
1385 """Re-commits using the current message, assumes the commit hook is in
1386 place.
1387 """
1388 log_desc = options.message or CreateDescriptionFromLog(args)
1389 git_command = ['commit', '--amend', '-m', log_desc]
1390 RunGit(git_command)
1391 new_log_desc = CreateDescriptionFromLog(args)
1392 if CHANGE_ID in new_log_desc:
1393 print 'git-cl: Added Change-Id to commit message.'
1394 else:
1395 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1396
1397
ukai@chromium.orge8077812012-02-03 03:41:46 +00001398def GerritUpload(options, args, cl):
1399 """upload the current branch to gerrit."""
1400 # We assume the remote called "origin" is the one we want.
1401 # It is probably not worthwhile to support different workflows.
1402 remote = 'origin'
1403 branch = 'master'
1404 if options.target_branch:
1405 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001407 change_desc = ChangeDescription(
1408 options.message or CreateDescriptionFromLog(args))
1409 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001410 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001412 if CHANGE_ID not in change_desc.description:
1413 AddChangeIdToCommitMessage(options, args)
1414 if options.reviewers:
1415 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
ukai@chromium.orge8077812012-02-03 03:41:46 +00001417 receive_options = []
1418 cc = cl.GetCCList().split(',')
1419 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001420 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001421 cc = filter(None, cc)
1422 if cc:
1423 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001424 if change_desc.get_reviewers():
1425 receive_options.extend(
1426 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
ukai@chromium.orge8077812012-02-03 03:41:46 +00001428 git_command = ['push']
1429 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001430 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001431 ' '.join(receive_options))
1432 git_command += [remote, 'HEAD:refs/for/' + branch]
1433 RunGit(git_command)
1434 # TODO(ukai): parse Change-Id: and set issue number?
1435 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437
ukai@chromium.orge8077812012-02-03 03:41:46 +00001438def RietveldUpload(options, args, cl):
1439 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1441 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442 if options.emulate_svn_auto_props:
1443 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444
1445 change_desc = None
1446
1447 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001448 if options.title:
1449 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001450 if options.message:
1451 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001452 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 print ("This branch is associated with issue %s. "
1454 "Adding patch to that issue." % cl.GetIssue())
1455 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001456 if options.title:
1457 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001458 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001459 change_desc = ChangeDescription(message)
1460 if options.reviewers:
1461 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001462 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001463 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001464
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001465 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 print "Description is empty; aborting."
1467 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001468
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001469 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001470 if change_desc.get_reviewers():
1471 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001472 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001474 DieWithError("Must specify reviewers to send email.")
1475 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001476
1477 # We check this before applying rietveld.private assuming that in
1478 # rietveld.cc only addresses which we can send private CLs to are listed
1479 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1480 # --private is specified explicitly on the command line.
1481 if options.private:
1482 logging.warn('rietveld.cc is ignored since private flag is specified. '
1483 'You need to review and add them manually if necessary.')
1484 cc = cl.GetCCListWithoutDefault()
1485 else:
1486 cc = cl.GetCCList()
1487 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001488 if cc:
1489 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001491 if options.private or settings.GetDefaultPrivateFlag() == "True":
1492 upload_args.append('--private')
1493
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001494 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001495 if not options.find_copies:
1496 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001497
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498 # Include the upstream repo's URL in the change -- this is useful for
1499 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001500 remote_url = cl.GetGitBaseUrlFromConfig()
1501 if not remote_url:
1502 if settings.GetIsGitSvn():
1503 # URL is dependent on the current directory.
1504 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1505 if data:
1506 keys = dict(line.split(': ', 1) for line in data.splitlines()
1507 if ': ' in line)
1508 remote_url = keys.get('URL', None)
1509 else:
1510 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1511 remote_url = (cl.GetRemoteUrl() + '@'
1512 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513 if remote_url:
1514 upload_args.extend(['--base_url', remote_url])
1515
1516 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001517 upload_args = ['upload'] + upload_args + args
1518 logging.info('upload.RealMain(%s)', upload_args)
1519 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001520 issue = int(issue)
1521 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001522 except KeyboardInterrupt:
1523 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001524 except:
1525 # If we got an exception after the user typed a description for their
1526 # change, back up the description before re-raising.
1527 if change_desc:
1528 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1529 print '\nGot exception while uploading -- saving description to %s\n' \
1530 % backup_path
1531 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001532 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 backup_file.close()
1534 raise
1535
1536 if not cl.GetIssue():
1537 cl.SetIssue(issue)
1538 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001539
1540 if options.use_commit_queue:
1541 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542 return 0
1543
1544
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001545def cleanup_list(l):
1546 """Fixes a list so that comma separated items are put as individual items.
1547
1548 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1549 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1550 """
1551 items = sum((i.split(',') for i in l), [])
1552 stripped_items = (i.strip() for i in items)
1553 return sorted(filter(None, stripped_items))
1554
1555
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001556@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001557def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001558 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001559 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1560 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001561 parser.add_option('--bypass-watchlists', action='store_true',
1562 dest='bypass_watchlists',
1563 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001564 parser.add_option('-f', action='store_true', dest='force',
1565 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001566 parser.add_option('-m', dest='message', help='message for patchset')
1567 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001568 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001569 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001570 help='reviewer email addresses')
1571 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001572 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001574 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001575 help='send email to reviewer immediately')
1576 parser.add_option("--emulate_svn_auto_props", action="store_true",
1577 dest="emulate_svn_auto_props",
1578 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001579 parser.add_option('-c', '--use-commit-queue', action='store_true',
1580 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001581 parser.add_option('--private', action='store_true',
1582 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001583 parser.add_option('--target_branch',
1584 help='When uploading to gerrit, remote branch to '
1585 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001586 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001587 (options, args) = parser.parse_args(args)
1588
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001589 if options.target_branch and not settings.GetIsGerrit():
1590 parser.error('Use --target_branch for non gerrit repository.')
1591
ukai@chromium.org259e4682012-10-25 07:36:33 +00001592 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001593 return 1
1594
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001595 options.reviewers = cleanup_list(options.reviewers)
1596 options.cc = cleanup_list(options.cc)
1597
ukai@chromium.orge8077812012-02-03 03:41:46 +00001598 cl = Changelist()
1599 if args:
1600 # TODO(ukai): is it ok for gerrit case?
1601 base_branch = args[0]
1602 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001603 # Default to diffing against common ancestor of upstream branch
1604 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001605 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001606
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001607 # Apply watchlists on upload.
1608 change = cl.GetChange(base_branch, None)
1609 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1610 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001611 if not options.bypass_watchlists:
1612 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001613
ukai@chromium.orge8077812012-02-03 03:41:46 +00001614 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001615 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001616 may_prompt=not options.force,
1617 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001618 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001619 if not hook_results.should_continue():
1620 return 1
1621 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001622 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001623
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001624 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001625 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001626 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001627 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001628 print ('The last upload made from this repository was patchset #%d but '
1629 'the most recent patchset on the server is #%d.'
1630 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001631 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1632 'from another machine or branch the patch you\'re uploading now '
1633 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001634 ask_for_data('About to upload; enter to confirm.')
1635
iannucci@chromium.org79540052012-10-19 23:15:26 +00001636 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001637 if settings.GetIsGerrit():
1638 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001639 ret = RietveldUpload(options, args, cl)
1640 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001641 git_set_branch_value('last-upload-hash',
1642 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001643
1644 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001645
1646
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001647def IsSubmoduleMergeCommit(ref):
1648 # When submodules are added to the repo, we expect there to be a single
1649 # non-git-svn merge commit at remote HEAD with a signature comment.
1650 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001651 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001652 return RunGit(cmd) != ''
1653
1654
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655def SendUpstream(parser, args, cmd):
1656 """Common code for CmdPush and CmdDCommit
1657
1658 Squashed commit into a single.
1659 Updates changelog with metadata (e.g. pointer to review).
1660 Pushes/dcommits the code upstream.
1661 Updates review and closes.
1662 """
1663 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1664 help='bypass upload presubmit hook')
1665 parser.add_option('-m', dest='message',
1666 help="override review description")
1667 parser.add_option('-f', action='store_true', dest='force',
1668 help="force yes to questions (don't prompt)")
1669 parser.add_option('-c', dest='contributor',
1670 help="external contributor for patch (appended to " +
1671 "description and used as author for git). Should be " +
1672 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001673 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674 (options, args) = parser.parse_args(args)
1675 cl = Changelist()
1676
1677 if not args or cmd == 'push':
1678 # Default to merging against our best guess of the upstream branch.
1679 args = [cl.GetUpstreamBranch()]
1680
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001681 if options.contributor:
1682 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1683 print "Please provide contibutor as 'First Last <email@example.com>'"
1684 return 1
1685
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001687 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001688
ukai@chromium.org259e4682012-10-25 07:36:33 +00001689 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690 return 1
1691
1692 # This rev-list syntax means "show all commits not in my branch that
1693 # are in base_branch".
1694 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1695 base_branch]).splitlines()
1696 if upstream_commits:
1697 print ('Base branch "%s" has %d commits '
1698 'not in this branch.' % (base_branch, len(upstream_commits)))
1699 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1700 return 1
1701
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001702 # This is the revision `svn dcommit` will commit on top of.
1703 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1704 '--pretty=format:%H'])
1705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001707 # If the base_head is a submodule merge commit, the first parent of the
1708 # base_head should be a git-svn commit, which is what we're interested in.
1709 base_svn_head = base_branch
1710 if base_has_submodules:
1711 base_svn_head += '^1'
1712
1713 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001714 if extra_commits:
1715 print ('This branch has %d additional commits not upstreamed yet.'
1716 % len(extra_commits.splitlines()))
1717 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1718 'before attempting to %s.' % (base_branch, cmd))
1719 return 1
1720
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001721 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001722 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001723 author = None
1724 if options.contributor:
1725 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001726 hook_results = cl.RunHook(
1727 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001728 may_prompt=not options.force,
1729 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001730 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001731 if not hook_results.should_continue():
1732 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001733
1734 if cmd == 'dcommit':
1735 # Check the tree status if the tree status URL is set.
1736 status = GetTreeStatus()
1737 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001738 print('The tree is closed. Please wait for it to reopen. Use '
1739 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740 return 1
1741 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001742 print('Unable to determine tree status. Please verify manually and '
1743 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001744 else:
1745 breakpad.SendStack(
1746 'GitClHooksBypassedCommit',
1747 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001748 (cl.GetRietveldServer(), cl.GetIssue()),
1749 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001751 change_desc = ChangeDescription(options.message)
1752 if not change_desc.description and cl.GetIssue():
1753 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001755 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001756 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001757 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001758 else:
1759 print 'No description set.'
1760 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1761 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001763 # Keep a separate copy for the commit message, because the commit message
1764 # contains the link to the Rietveld issue, while the Rietveld message contains
1765 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001766 # Keep a separate copy for the commit message.
1767 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001768 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001769
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001770 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001771 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001772 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001774 commit_desc.append_footer('Patch from %s.' % options.contributor)
1775
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001776 print('Description:')
1777 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001778
1779 branches = [base_branch, cl.GetBranchRef()]
1780 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001781 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001782 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001783
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001784 # We want to squash all this branch's commits into one commit with the proper
1785 # description. We do this by doing a "reset --soft" to the base branch (which
1786 # keeps the working copy the same), then dcommitting that. If origin/master
1787 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1788 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001790 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1791 # Delete the branches if they exist.
1792 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1793 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1794 result = RunGitWithCode(showref_cmd)
1795 if result[0] == 0:
1796 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797
1798 # We might be in a directory that's present in this branch but not in the
1799 # trunk. Move up to the top of the tree so that git commands that expect a
1800 # valid CWD won't fail after we check out the merge branch.
1801 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1802 if rel_base_path:
1803 os.chdir(rel_base_path)
1804
1805 # Stuff our change into the merge branch.
1806 # We wrap in a try...finally block so if anything goes wrong,
1807 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001808 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001809 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001810 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1811 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001813 RunGit(
1814 [
1815 'commit', '--author', options.contributor,
1816 '-m', commit_desc.description,
1817 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001819 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001820 if base_has_submodules:
1821 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1822 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1823 RunGit(['checkout', CHERRY_PICK_BRANCH])
1824 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001825 if cmd == 'push':
1826 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001827 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828 retcode, output = RunGitWithCode(
1829 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1830 logging.debug(output)
1831 else:
1832 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001833 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001834 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001835 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 finally:
1837 # And then swap back to the original branch and clean up.
1838 RunGit(['checkout', '-q', cl.GetBranch()])
1839 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001840 if base_has_submodules:
1841 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842
1843 if cl.GetIssue():
1844 if cmd == 'dcommit' and 'Committed r' in output:
1845 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1846 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001847 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1848 for l in output.splitlines(False))
1849 match = filter(None, match)
1850 if len(match) != 1:
1851 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1852 output)
1853 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854 else:
1855 return 1
1856 viewvc_url = settings.GetViewVCUrl()
1857 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001858 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001859 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001860 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861 print ('Closing issue '
1862 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001863 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001865 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001866 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001867 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001868 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1869 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001870 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001871
1872 if retcode == 0:
1873 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1874 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001875 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001876
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001877 return 0
1878
1879
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001880@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001882 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001883 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001884 message = """This doesn't appear to be an SVN repository.
1885If your project has a git mirror with an upstream SVN master, you probably need
1886to run 'git svn init', see your project's git mirror documentation.
1887If your project has a true writeable upstream repository, you probably want
1888to run 'git cl push' instead.
1889Choose wisely, if you get this wrong, your commit might appear to succeed but
1890will instead be silently ignored."""
1891 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001892 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893 return SendUpstream(parser, args, 'dcommit')
1894
1895
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001896@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001898 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001899 if settings.GetIsGitSvn():
1900 print('This appears to be an SVN repository.')
1901 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001902 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 return SendUpstream(parser, args, 'push')
1904
1905
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001906@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001908 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 parser.add_option('-b', dest='newbranch',
1910 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001911 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001913 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1914 help='Change to the directory DIR immediately, '
1915 'before doing anything else.')
1916 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001917 help='failed patches spew .rej files rather than '
1918 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1920 help="don't commit after patch applies")
1921 (options, args) = parser.parse_args(args)
1922 if len(args) != 1:
1923 parser.print_help()
1924 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001925 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001927 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001928 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001929
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001930 if options.newbranch:
1931 if options.force:
1932 RunGit(['branch', '-D', options.newbranch],
1933 stderr=subprocess2.PIPE, error_ok=True)
1934 RunGit(['checkout', '-b', options.newbranch,
1935 Changelist().GetUpstreamBranch()])
1936
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001937 return PatchIssue(issue_arg, options.reject, options.nocommit,
1938 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001939
1940
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001941def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001942 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001944 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001945 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001946 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001947 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001949 # Assume it's a URL to the patch. Default to https.
1950 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001951 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001952 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001953 DieWithError('Must pass an issue ID or full URL for '
1954 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001955 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001956 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001957 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959 # Switch up to the top-level directory, if necessary, in preparation for
1960 # applying the patch.
1961 top = RunGit(['rev-parse', '--show-cdup']).strip()
1962 if top:
1963 os.chdir(top)
1964
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 # Git patches have a/ at the beginning of source paths. We strip that out
1966 # with a sed script rather than the -p flag to patch so we can feed either
1967 # Git or svn-style patches into the same apply command.
1968 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001969 try:
1970 patch_data = subprocess2.check_output(
1971 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1972 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 DieWithError('Git patch mungling failed.')
1974 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001975 env = os.environ.copy()
1976 # 'cat' is a magical git string that disables pagers on all platforms.
1977 env['GIT_PAGER'] = 'cat'
1978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 # We use "git apply" to apply the patch instead of "patch" so that we can
1980 # pick up file adds.
1981 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001982 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001983 if directory:
1984 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001985 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001987 elif IsGitVersionAtLeast('1.7.12'):
1988 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001989 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001990 subprocess2.check_call(cmd, env=env,
1991 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001992 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993 DieWithError('Failed to apply the patch')
1994
1995 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001996 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1998 cl = Changelist()
1999 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002000 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002001 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002 else:
2003 print "Patch applied to index."
2004 return 0
2005
2006
2007def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002008 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009 # Provide a wrapper for git svn rebase to help avoid accidental
2010 # git svn dcommit.
2011 # It's the only command that doesn't use parser at all since we just defer
2012 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002013 env = os.environ.copy()
2014 # 'cat' is a magical git string that disables pagers on all platforms.
2015 env['GIT_PAGER'] = 'cat'
2016
2017 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018
2019
2020def GetTreeStatus():
2021 """Fetches the tree status and returns either 'open', 'closed',
2022 'unknown' or 'unset'."""
2023 url = settings.GetTreeStatusUrl(error_ok=True)
2024 if url:
2025 status = urllib2.urlopen(url).read().lower()
2026 if status.find('closed') != -1 or status == '0':
2027 return 'closed'
2028 elif status.find('open') != -1 or status == '1':
2029 return 'open'
2030 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002031 return 'unset'
2032
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002034def GetTreeStatusReason():
2035 """Fetches the tree status from a json url and returns the message
2036 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002037 url = settings.GetTreeStatusUrl()
2038 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039 connection = urllib2.urlopen(json_url)
2040 status = json.loads(connection.read())
2041 connection.close()
2042 return status['message']
2043
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002044
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002046 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002047 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048 status = GetTreeStatus()
2049 if 'unset' == status:
2050 print 'You must configure your tree status URL by running "git cl config".'
2051 return 2
2052
2053 print "The tree is %s" % status
2054 print
2055 print GetTreeStatusReason()
2056 if status != 'open':
2057 return 1
2058 return 0
2059
2060
maruel@chromium.org15192402012-09-06 12:38:29 +00002061def CMDtry(parser, args):
2062 """Triggers a try job through Rietveld."""
2063 group = optparse.OptionGroup(parser, "Try job options")
2064 group.add_option(
2065 "-b", "--bot", action="append",
2066 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2067 "times to specify multiple builders. ex: "
2068 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2069 "the try server waterfall for the builders name and the tests "
2070 "available. Can also be used to specify gtest_filter, e.g. "
2071 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2072 group.add_option(
2073 "-r", "--revision",
2074 help="Revision to use for the try job; default: the "
2075 "revision will be determined by the try server; see "
2076 "its waterfall for more info")
2077 group.add_option(
2078 "-c", "--clobber", action="store_true", default=False,
2079 help="Force a clobber before building; e.g. don't do an "
2080 "incremental build")
2081 group.add_option(
2082 "--project",
2083 help="Override which project to use. Projects are defined "
2084 "server-side to define what default bot set to use")
2085 group.add_option(
2086 "-t", "--testfilter", action="append", default=[],
2087 help=("Apply a testfilter to all the selected builders. Unless the "
2088 "builders configurations are similar, use multiple "
2089 "--bot <builder>:<test> arguments."))
2090 group.add_option(
2091 "-n", "--name", help="Try job name; default to current branch name")
2092 parser.add_option_group(group)
2093 options, args = parser.parse_args(args)
2094
2095 if args:
2096 parser.error('Unknown arguments: %s' % args)
2097
2098 cl = Changelist()
2099 if not cl.GetIssue():
2100 parser.error('Need to upload first')
2101
2102 if not options.name:
2103 options.name = cl.GetBranch()
2104
2105 # Process --bot and --testfilter.
2106 if not options.bot:
2107 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002108 change = cl.GetChange(
2109 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2110 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002111 options.bot = presubmit_support.DoGetTrySlaves(
2112 change,
2113 change.LocalPaths(),
2114 settings.GetRoot(),
2115 None,
2116 None,
2117 options.verbose,
2118 sys.stdout)
2119 if not options.bot:
2120 parser.error('No default try builder to try, use --bot')
2121
2122 builders_and_tests = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002123 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2124 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2125
2126 for bot in old_style:
maruel@chromium.org15192402012-09-06 12:38:29 +00002127 if ':' in bot:
2128 builder, tests = bot.split(':', 1)
2129 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2130 elif ',' in bot:
2131 parser.error('Specify one bot per --bot flag')
2132 else:
2133 builders_and_tests.setdefault(bot, []).append('defaulttests')
2134
stip@chromium.org43064fd2013-12-18 20:07:44 +00002135 for bot, tests in new_style:
2136 builders_and_tests.setdefault(bot, []).extend(tests)
2137
maruel@chromium.org15192402012-09-06 12:38:29 +00002138 if options.testfilter:
2139 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2140 builders_and_tests = dict(
2141 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2142 if t != ['compile'])
2143
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002144 if any('triggered' in b for b in builders_and_tests):
2145 print >> sys.stderr, (
2146 'ERROR You are trying to send a job to a triggered bot. This type of'
2147 ' bot requires an\ninitial job from a parent (usually a builder). '
2148 'Instead send your job to the parent.\n'
2149 'Bot list: %s' % builders_and_tests)
2150 return 1
2151
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002152 patchset = cl.GetMostRecentPatchset()
2153 if patchset and patchset != cl.GetPatchset():
2154 print(
2155 '\nWARNING Mismatch between local config and server. Did a previous '
2156 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2157 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002158 try:
2159 cl.RpcServer().trigger_try_jobs(
2160 cl.GetIssue(), patchset, options.name, options.clobber,
2161 options.revision, builders_and_tests)
2162 except urllib2.HTTPError, e:
2163 if e.code == 404:
2164 print('404 from rietveld; '
2165 'did you mean to use "git try" instead of "git cl try"?')
2166 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002167 print('Tried jobs on:')
2168 length = max(len(builder) for builder in builders_and_tests)
2169 for builder in sorted(builders_and_tests):
2170 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002171 return 0
2172
2173
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002174@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002175def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002176 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002177 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002178 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002179 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002181 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002182 if args:
2183 # One arg means set upstream branch.
2184 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2185 cl = Changelist()
2186 print "Upstream branch set to " + cl.GetUpstreamBranch()
2187 else:
2188 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002189 return 0
2190
2191
thestig@chromium.org00858c82013-12-02 23:08:03 +00002192def CMDweb(parser, args):
2193 """Opens the current CL in the web browser."""
2194 _, args = parser.parse_args(args)
2195 if args:
2196 parser.error('Unrecognized args: %s' % ' '.join(args))
2197
2198 issue_url = Changelist().GetIssueURL()
2199 if not issue_url:
2200 print >> sys.stderr, 'ERROR No issue to open'
2201 return 1
2202
2203 webbrowser.open(issue_url)
2204 return 0
2205
2206
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002207def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002208 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002209 _, args = parser.parse_args(args)
2210 if args:
2211 parser.error('Unrecognized args: %s' % ' '.join(args))
2212 cl = Changelist()
2213 cl.SetFlag('commit', '1')
2214 return 0
2215
2216
groby@chromium.org411034a2013-02-26 15:12:01 +00002217def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002218 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002219 _, args = parser.parse_args(args)
2220 if args:
2221 parser.error('Unrecognized args: %s' % ' '.join(args))
2222 cl = Changelist()
2223 # Ensure there actually is an issue to close.
2224 cl.GetDescription()
2225 cl.CloseIssue()
2226 return 0
2227
2228
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002229def CMDdiff(parser, args):
2230 """shows differences between local tree and last upload."""
2231 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002232 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002233 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002234 if not issue:
2235 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002236 TMP_BRANCH = 'git-cl-diff'
2237 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2238
2239 # Create a new branch based on the merge-base
2240 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2241 try:
2242 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002243 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002244 if rtn != 0:
2245 return rtn
2246
2247 # Switch back to starting brand and diff against the temporary
2248 # branch containing the latest rietveld patch.
2249 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2250 finally:
2251 RunGit(['checkout', '-q', branch])
2252 RunGit(['branch', '-D', TMP_BRANCH])
2253
2254 return 0
2255
2256
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002257def CMDowners(parser, args):
2258 """interactively find the owners for reviewing"""
2259 parser.add_option(
2260 '--no-color',
2261 action='store_true',
2262 help='Use this option to disable color output')
2263 options, args = parser.parse_args(args)
2264
2265 author = RunGit(['config', 'user.email']).strip() or None
2266
2267 cl = Changelist()
2268
2269 if args:
2270 if len(args) > 1:
2271 parser.error('Unknown args')
2272 base_branch = args[0]
2273 else:
2274 # Default to diffing against the common ancestor of the upstream branch.
2275 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2276
2277 change = cl.GetChange(base_branch, None)
2278 return owners_finder.OwnersFinder(
2279 [f.LocalPath() for f in
2280 cl.GetChange(base_branch, None).AffectedFiles()],
2281 change.RepositoryRoot(), author,
2282 fopen=file, os_path=os.path, glob=glob.glob,
2283 disable_color=options.no_color).run()
2284
2285
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002286def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002287 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002288 CLANG_EXTS = ['.cc', '.cpp', '.h']
2289 parser.add_option('--full', action='store_true', default=False)
2290 opts, args = parser.parse_args(args)
2291 if args:
2292 parser.error('Unrecognized args: %s' % ' '.join(args))
2293
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002294 # git diff generates paths against the root of the repository. Change
2295 # to that directory so clang-format can find files even within subdirs.
2296 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2297 if rel_base_path:
2298 os.chdir(rel_base_path)
2299
digit@chromium.org29e47272013-05-17 17:01:46 +00002300 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002301 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002302 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002303 # Only list the names of modified files.
2304 diff_cmd.append('--name-only')
2305 else:
2306 # Only generate context-less patches.
2307 diff_cmd.append('-U0')
2308
2309 # Grab the merge-base commit, i.e. the upstream commit of the current
2310 # branch when it was created or the last time it was rebased. This is
2311 # to cover the case where the user may have called "git fetch origin",
2312 # moving the origin branch to a newer commit, but hasn't rebased yet.
2313 upstream_commit = None
2314 cl = Changelist()
2315 upstream_branch = cl.GetUpstreamBranch()
2316 if upstream_branch:
2317 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2318 upstream_commit = upstream_commit.strip()
2319
2320 if not upstream_commit:
2321 DieWithError('Could not find base commit for this branch. '
2322 'Are you in detached state?')
2323
2324 diff_cmd.append(upstream_commit)
2325
2326 # Handle source file filtering.
2327 diff_cmd.append('--')
2328 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2329 diff_output = RunGit(diff_cmd)
2330
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002331 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2332
digit@chromium.org29e47272013-05-17 17:01:46 +00002333 if opts.full:
2334 # diff_output is a list of files to send to clang-format.
2335 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002336 if not files:
2337 print "Nothing to format."
2338 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002339 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2340 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002341 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002342 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002343 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2344 'clang-format-diff.py')
2345 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002346 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002347 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002348
2349 # Newer versions of clang-format-diff.py require an explicit -i flag
2350 # to apply the edits to files, otherwise it just displays a diff.
2351 # Probe the usage string to verify if this is needed.
2352 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2353 if '[-i]' in help_text:
2354 cmd.append('-i')
2355
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002356 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002357
2358 return 0
2359
2360
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002361class OptionParser(optparse.OptionParser):
2362 """Creates the option parse and add --verbose support."""
2363 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002364 optparse.OptionParser.__init__(
2365 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002366 self.add_option(
2367 '-v', '--verbose', action='count', default=0,
2368 help='Use 2 times for more debugging info')
2369
2370 def parse_args(self, args=None, values=None):
2371 options, args = optparse.OptionParser.parse_args(self, args, values)
2372 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2373 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2374 return options, args
2375
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002377def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002378 if sys.hexversion < 0x02060000:
2379 print >> sys.stderr, (
2380 '\nYour python version %s is unsupported, please upgrade.\n' %
2381 sys.version.split(' ', 1)[0])
2382 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002383
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002384 # Reload settings.
2385 global settings
2386 settings = Settings()
2387
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002388 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002389 dispatcher = subcommand.CommandDispatcher(__name__)
2390 try:
2391 return dispatcher.execute(OptionParser(), argv)
2392 except urllib2.HTTPError, e:
2393 if e.code != 500:
2394 raise
2395 DieWithError(
2396 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2397 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002398
2399
2400if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002401 # These affect sys.stdout so do it outside of main() to simplify mocks in
2402 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002403 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002404 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002405 sys.exit(main(sys.argv[1:]))