blob: 2bf1b9811ef670bf948c815515f30a39a7c12472 [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
pgervais@chromium.org91141372014-01-09 23:27:20 +00001447 if options.email is not None:
1448 upload_args.extend(['--email', options.email])
1449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001451 if options.title:
1452 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001453 if options.message:
1454 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001455 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 print ("This branch is associated with issue %s. "
1457 "Adding patch to that issue." % cl.GetIssue())
1458 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001459 if options.title:
1460 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001461 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001462 change_desc = ChangeDescription(message)
1463 if options.reviewers:
1464 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001465 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001466 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001467
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001468 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469 print "Description is empty; aborting."
1470 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001471
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001472 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 if change_desc.get_reviewers():
1474 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001475 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001476 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001477 DieWithError("Must specify reviewers to send email.")
1478 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001479
1480 # We check this before applying rietveld.private assuming that in
1481 # rietveld.cc only addresses which we can send private CLs to are listed
1482 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1483 # --private is specified explicitly on the command line.
1484 if options.private:
1485 logging.warn('rietveld.cc is ignored since private flag is specified. '
1486 'You need to review and add them manually if necessary.')
1487 cc = cl.GetCCListWithoutDefault()
1488 else:
1489 cc = cl.GetCCList()
1490 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001491 if cc:
1492 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001494 if options.private or settings.GetDefaultPrivateFlag() == "True":
1495 upload_args.append('--private')
1496
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001497 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001498 if not options.find_copies:
1499 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 # Include the upstream repo's URL in the change -- this is useful for
1502 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001503 remote_url = cl.GetGitBaseUrlFromConfig()
1504 if not remote_url:
1505 if settings.GetIsGitSvn():
1506 # URL is dependent on the current directory.
1507 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1508 if data:
1509 keys = dict(line.split(': ', 1) for line in data.splitlines()
1510 if ': ' in line)
1511 remote_url = keys.get('URL', None)
1512 else:
1513 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1514 remote_url = (cl.GetRemoteUrl() + '@'
1515 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516 if remote_url:
1517 upload_args.extend(['--base_url', remote_url])
1518
1519 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001520 upload_args = ['upload'] + upload_args + args
1521 logging.info('upload.RealMain(%s)', upload_args)
1522 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001523 issue = int(issue)
1524 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001525 except KeyboardInterrupt:
1526 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 except:
1528 # If we got an exception after the user typed a description for their
1529 # change, back up the description before re-raising.
1530 if change_desc:
1531 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1532 print '\nGot exception while uploading -- saving description to %s\n' \
1533 % backup_path
1534 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001535 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 backup_file.close()
1537 raise
1538
1539 if not cl.GetIssue():
1540 cl.SetIssue(issue)
1541 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001542
1543 if options.use_commit_queue:
1544 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 return 0
1546
1547
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001548def cleanup_list(l):
1549 """Fixes a list so that comma separated items are put as individual items.
1550
1551 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1552 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1553 """
1554 items = sum((i.split(',') for i in l), [])
1555 stripped_items = (i.strip() for i in items)
1556 return sorted(filter(None, stripped_items))
1557
1558
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001559@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001560def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001561 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001562 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1563 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001564 parser.add_option('--bypass-watchlists', action='store_true',
1565 dest='bypass_watchlists',
1566 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001567 parser.add_option('-f', action='store_true', dest='force',
1568 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001569 parser.add_option('-m', dest='message', help='message for patchset')
1570 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001571 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001572 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573 help='reviewer email addresses')
1574 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001575 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001576 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001577 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001578 help='send email to reviewer immediately')
1579 parser.add_option("--emulate_svn_auto_props", action="store_true",
1580 dest="emulate_svn_auto_props",
1581 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001582 parser.add_option('-c', '--use-commit-queue', action='store_true',
1583 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001584 parser.add_option('--private', action='store_true',
1585 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001586 parser.add_option('--target_branch',
1587 help='When uploading to gerrit, remote branch to '
1588 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001589 parser.add_option('--email', default=None,
1590 help='email address to use to connect to Rietveld')
1591
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001592 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001593 (options, args) = parser.parse_args(args)
1594
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001595 if options.target_branch and not settings.GetIsGerrit():
1596 parser.error('Use --target_branch for non gerrit repository.')
1597
ukai@chromium.org259e4682012-10-25 07:36:33 +00001598 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001599 return 1
1600
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001601 options.reviewers = cleanup_list(options.reviewers)
1602 options.cc = cleanup_list(options.cc)
1603
ukai@chromium.orge8077812012-02-03 03:41:46 +00001604 cl = Changelist()
1605 if args:
1606 # TODO(ukai): is it ok for gerrit case?
1607 base_branch = args[0]
1608 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001609 # Default to diffing against common ancestor of upstream branch
1610 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001611 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001612
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001613 # Apply watchlists on upload.
1614 change = cl.GetChange(base_branch, None)
1615 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1616 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001617 if not options.bypass_watchlists:
1618 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001619
ukai@chromium.orge8077812012-02-03 03:41:46 +00001620 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001621 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001622 may_prompt=not options.force,
1623 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001624 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001625 if not hook_results.should_continue():
1626 return 1
1627 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001628 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001629
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001630 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001631 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001632 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001633 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001634 print ('The last upload made from this repository was patchset #%d but '
1635 'the most recent patchset on the server is #%d.'
1636 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001637 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1638 'from another machine or branch the patch you\'re uploading now '
1639 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001640 ask_for_data('About to upload; enter to confirm.')
1641
iannucci@chromium.org79540052012-10-19 23:15:26 +00001642 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001643 if settings.GetIsGerrit():
1644 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001645 ret = RietveldUpload(options, args, cl)
1646 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001647 git_set_branch_value('last-upload-hash',
1648 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001649
1650 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001651
1652
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001653def IsSubmoduleMergeCommit(ref):
1654 # When submodules are added to the repo, we expect there to be a single
1655 # non-git-svn merge commit at remote HEAD with a signature comment.
1656 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001657 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001658 return RunGit(cmd) != ''
1659
1660
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001661def SendUpstream(parser, args, cmd):
1662 """Common code for CmdPush and CmdDCommit
1663
1664 Squashed commit into a single.
1665 Updates changelog with metadata (e.g. pointer to review).
1666 Pushes/dcommits the code upstream.
1667 Updates review and closes.
1668 """
1669 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1670 help='bypass upload presubmit hook')
1671 parser.add_option('-m', dest='message',
1672 help="override review description")
1673 parser.add_option('-f', action='store_true', dest='force',
1674 help="force yes to questions (don't prompt)")
1675 parser.add_option('-c', dest='contributor',
1676 help="external contributor for patch (appended to " +
1677 "description and used as author for git). Should be " +
1678 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001679 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680 (options, args) = parser.parse_args(args)
1681 cl = Changelist()
1682
1683 if not args or cmd == 'push':
1684 # Default to merging against our best guess of the upstream branch.
1685 args = [cl.GetUpstreamBranch()]
1686
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001687 if options.contributor:
1688 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1689 print "Please provide contibutor as 'First Last <email@example.com>'"
1690 return 1
1691
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001693 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001694
ukai@chromium.org259e4682012-10-25 07:36:33 +00001695 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001696 return 1
1697
1698 # This rev-list syntax means "show all commits not in my branch that
1699 # are in base_branch".
1700 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1701 base_branch]).splitlines()
1702 if upstream_commits:
1703 print ('Base branch "%s" has %d commits '
1704 'not in this branch.' % (base_branch, len(upstream_commits)))
1705 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1706 return 1
1707
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001708 # This is the revision `svn dcommit` will commit on top of.
1709 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1710 '--pretty=format:%H'])
1711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001712 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001713 # If the base_head is a submodule merge commit, the first parent of the
1714 # base_head should be a git-svn commit, which is what we're interested in.
1715 base_svn_head = base_branch
1716 if base_has_submodules:
1717 base_svn_head += '^1'
1718
1719 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720 if extra_commits:
1721 print ('This branch has %d additional commits not upstreamed yet.'
1722 % len(extra_commits.splitlines()))
1723 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1724 'before attempting to %s.' % (base_branch, cmd))
1725 return 1
1726
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001727 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001728 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001729 author = None
1730 if options.contributor:
1731 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001732 hook_results = cl.RunHook(
1733 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001734 may_prompt=not options.force,
1735 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001736 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001737 if not hook_results.should_continue():
1738 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739
1740 if cmd == 'dcommit':
1741 # Check the tree status if the tree status URL is set.
1742 status = GetTreeStatus()
1743 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001744 print('The tree is closed. Please wait for it to reopen. Use '
1745 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746 return 1
1747 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001748 print('Unable to determine tree status. Please verify manually and '
1749 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001750 else:
1751 breakpad.SendStack(
1752 'GitClHooksBypassedCommit',
1753 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001754 (cl.GetRietveldServer(), cl.GetIssue()),
1755 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001757 change_desc = ChangeDescription(options.message)
1758 if not change_desc.description and cl.GetIssue():
1759 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001761 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001762 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001763 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001764 else:
1765 print 'No description set.'
1766 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1767 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001769 # Keep a separate copy for the commit message, because the commit message
1770 # contains the link to the Rietveld issue, while the Rietveld message contains
1771 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001772 # Keep a separate copy for the commit message.
1773 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001774 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001775
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001776 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001777 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001778 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001780 commit_desc.append_footer('Patch from %s.' % options.contributor)
1781
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001782 print('Description:')
1783 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784
1785 branches = [base_branch, cl.GetBranchRef()]
1786 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001787 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001788 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001790 # We want to squash all this branch's commits into one commit with the proper
1791 # description. We do this by doing a "reset --soft" to the base branch (which
1792 # keeps the working copy the same), then dcommitting that. If origin/master
1793 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1794 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001796 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1797 # Delete the branches if they exist.
1798 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1799 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1800 result = RunGitWithCode(showref_cmd)
1801 if result[0] == 0:
1802 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803
1804 # We might be in a directory that's present in this branch but not in the
1805 # trunk. Move up to the top of the tree so that git commands that expect a
1806 # valid CWD won't fail after we check out the merge branch.
1807 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1808 if rel_base_path:
1809 os.chdir(rel_base_path)
1810
1811 # Stuff our change into the merge branch.
1812 # We wrap in a try...finally block so if anything goes wrong,
1813 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001814 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001816 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1817 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001819 RunGit(
1820 [
1821 'commit', '--author', options.contributor,
1822 '-m', commit_desc.description,
1823 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001825 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001826 if base_has_submodules:
1827 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1828 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1829 RunGit(['checkout', CHERRY_PICK_BRANCH])
1830 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831 if cmd == 'push':
1832 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001833 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834 retcode, output = RunGitWithCode(
1835 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1836 logging.debug(output)
1837 else:
1838 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001839 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001840 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001841 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842 finally:
1843 # And then swap back to the original branch and clean up.
1844 RunGit(['checkout', '-q', cl.GetBranch()])
1845 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001846 if base_has_submodules:
1847 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001848
1849 if cl.GetIssue():
1850 if cmd == 'dcommit' and 'Committed r' in output:
1851 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1852 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001853 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1854 for l in output.splitlines(False))
1855 match = filter(None, match)
1856 if len(match) != 1:
1857 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1858 output)
1859 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 else:
1861 return 1
1862 viewvc_url = settings.GetViewVCUrl()
1863 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001864 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001865 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001866 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867 print ('Closing issue '
1868 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001869 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001870 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001871 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001872 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001873 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001874 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1875 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001876 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001877
1878 if retcode == 0:
1879 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1880 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001881 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001882
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001883 return 0
1884
1885
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001886@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001888 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001889 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001890 message = """This doesn't appear to be an SVN repository.
1891If your project has a git mirror with an upstream SVN master, you probably need
1892to run 'git svn init', see your project's git mirror documentation.
1893If your project has a true writeable upstream repository, you probably want
1894to run 'git cl push' instead.
1895Choose wisely, if you get this wrong, your commit might appear to succeed but
1896will instead be silently ignored."""
1897 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001898 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001899 return SendUpstream(parser, args, 'dcommit')
1900
1901
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001902@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001904 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 if settings.GetIsGitSvn():
1906 print('This appears to be an SVN repository.')
1907 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001908 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 return SendUpstream(parser, args, 'push')
1910
1911
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001912@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001914 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915 parser.add_option('-b', dest='newbranch',
1916 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001917 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001919 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1920 help='Change to the directory DIR immediately, '
1921 'before doing anything else.')
1922 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001923 help='failed patches spew .rej files rather than '
1924 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1926 help="don't commit after patch applies")
1927 (options, args) = parser.parse_args(args)
1928 if len(args) != 1:
1929 parser.print_help()
1930 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001931 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001933 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001934 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001935
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001936 if options.newbranch:
1937 if options.force:
1938 RunGit(['branch', '-D', options.newbranch],
1939 stderr=subprocess2.PIPE, error_ok=True)
1940 RunGit(['checkout', '-b', options.newbranch,
1941 Changelist().GetUpstreamBranch()])
1942
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001943 return PatchIssue(issue_arg, options.reject, options.nocommit,
1944 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001945
1946
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001947def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001948 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001950 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001951 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001952 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001953 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001954 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001955 # Assume it's a URL to the patch. Default to https.
1956 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001957 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001958 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959 DieWithError('Must pass an issue ID or full URL for '
1960 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001961 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001962 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001963 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 # Switch up to the top-level directory, if necessary, in preparation for
1966 # applying the patch.
1967 top = RunGit(['rev-parse', '--show-cdup']).strip()
1968 if top:
1969 os.chdir(top)
1970
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971 # Git patches have a/ at the beginning of source paths. We strip that out
1972 # with a sed script rather than the -p flag to patch so we can feed either
1973 # Git or svn-style patches into the same apply command.
1974 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001975 try:
1976 patch_data = subprocess2.check_output(
1977 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1978 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 DieWithError('Git patch mungling failed.')
1980 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001981 env = os.environ.copy()
1982 # 'cat' is a magical git string that disables pagers on all platforms.
1983 env['GIT_PAGER'] = 'cat'
1984
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985 # We use "git apply" to apply the patch instead of "patch" so that we can
1986 # pick up file adds.
1987 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001988 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001989 if directory:
1990 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001991 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001993 elif IsGitVersionAtLeast('1.7.12'):
1994 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001995 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001996 subprocess2.check_call(cmd, env=env,
1997 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001998 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 DieWithError('Failed to apply the patch')
2000
2001 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002002 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002003 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2004 cl = Changelist()
2005 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002006 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002007 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002008 else:
2009 print "Patch applied to index."
2010 return 0
2011
2012
2013def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002014 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015 # Provide a wrapper for git svn rebase to help avoid accidental
2016 # git svn dcommit.
2017 # It's the only command that doesn't use parser at all since we just defer
2018 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002019 env = os.environ.copy()
2020 # 'cat' is a magical git string that disables pagers on all platforms.
2021 env['GIT_PAGER'] = 'cat'
2022
2023 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024
2025
2026def GetTreeStatus():
2027 """Fetches the tree status and returns either 'open', 'closed',
2028 'unknown' or 'unset'."""
2029 url = settings.GetTreeStatusUrl(error_ok=True)
2030 if url:
2031 status = urllib2.urlopen(url).read().lower()
2032 if status.find('closed') != -1 or status == '0':
2033 return 'closed'
2034 elif status.find('open') != -1 or status == '1':
2035 return 'open'
2036 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002037 return 'unset'
2038
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002040def GetTreeStatusReason():
2041 """Fetches the tree status from a json url and returns the message
2042 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002043 url = settings.GetTreeStatusUrl()
2044 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045 connection = urllib2.urlopen(json_url)
2046 status = json.loads(connection.read())
2047 connection.close()
2048 return status['message']
2049
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002051def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002052 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002053 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054 status = GetTreeStatus()
2055 if 'unset' == status:
2056 print 'You must configure your tree status URL by running "git cl config".'
2057 return 2
2058
2059 print "The tree is %s" % status
2060 print
2061 print GetTreeStatusReason()
2062 if status != 'open':
2063 return 1
2064 return 0
2065
2066
maruel@chromium.org15192402012-09-06 12:38:29 +00002067def CMDtry(parser, args):
2068 """Triggers a try job through Rietveld."""
2069 group = optparse.OptionGroup(parser, "Try job options")
2070 group.add_option(
2071 "-b", "--bot", action="append",
2072 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2073 "times to specify multiple builders. ex: "
2074 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2075 "the try server waterfall for the builders name and the tests "
2076 "available. Can also be used to specify gtest_filter, e.g. "
2077 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2078 group.add_option(
2079 "-r", "--revision",
2080 help="Revision to use for the try job; default: the "
2081 "revision will be determined by the try server; see "
2082 "its waterfall for more info")
2083 group.add_option(
2084 "-c", "--clobber", action="store_true", default=False,
2085 help="Force a clobber before building; e.g. don't do an "
2086 "incremental build")
2087 group.add_option(
2088 "--project",
2089 help="Override which project to use. Projects are defined "
2090 "server-side to define what default bot set to use")
2091 group.add_option(
2092 "-t", "--testfilter", action="append", default=[],
2093 help=("Apply a testfilter to all the selected builders. Unless the "
2094 "builders configurations are similar, use multiple "
2095 "--bot <builder>:<test> arguments."))
2096 group.add_option(
2097 "-n", "--name", help="Try job name; default to current branch name")
2098 parser.add_option_group(group)
2099 options, args = parser.parse_args(args)
2100
2101 if args:
2102 parser.error('Unknown arguments: %s' % args)
2103
2104 cl = Changelist()
2105 if not cl.GetIssue():
2106 parser.error('Need to upload first')
2107
2108 if not options.name:
2109 options.name = cl.GetBranch()
2110
2111 # Process --bot and --testfilter.
2112 if not options.bot:
2113 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002114 change = cl.GetChange(
2115 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2116 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002117 options.bot = presubmit_support.DoGetTrySlaves(
2118 change,
2119 change.LocalPaths(),
2120 settings.GetRoot(),
2121 None,
2122 None,
2123 options.verbose,
2124 sys.stdout)
2125 if not options.bot:
2126 parser.error('No default try builder to try, use --bot')
2127
2128 builders_and_tests = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002129 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2130 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2131
2132 for bot in old_style:
maruel@chromium.org15192402012-09-06 12:38:29 +00002133 if ':' in bot:
2134 builder, tests = bot.split(':', 1)
2135 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2136 elif ',' in bot:
2137 parser.error('Specify one bot per --bot flag')
2138 else:
2139 builders_and_tests.setdefault(bot, []).append('defaulttests')
2140
stip@chromium.org43064fd2013-12-18 20:07:44 +00002141 for bot, tests in new_style:
2142 builders_and_tests.setdefault(bot, []).extend(tests)
2143
maruel@chromium.org15192402012-09-06 12:38:29 +00002144 if options.testfilter:
2145 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2146 builders_and_tests = dict(
2147 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2148 if t != ['compile'])
2149
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002150 if any('triggered' in b for b in builders_and_tests):
2151 print >> sys.stderr, (
2152 'ERROR You are trying to send a job to a triggered bot. This type of'
2153 ' bot requires an\ninitial job from a parent (usually a builder). '
2154 'Instead send your job to the parent.\n'
2155 'Bot list: %s' % builders_and_tests)
2156 return 1
2157
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002158 patchset = cl.GetMostRecentPatchset()
2159 if patchset and patchset != cl.GetPatchset():
2160 print(
2161 '\nWARNING Mismatch between local config and server. Did a previous '
2162 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2163 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002164 try:
2165 cl.RpcServer().trigger_try_jobs(
2166 cl.GetIssue(), patchset, options.name, options.clobber,
2167 options.revision, builders_and_tests)
2168 except urllib2.HTTPError, e:
2169 if e.code == 404:
2170 print('404 from rietveld; '
2171 'did you mean to use "git try" instead of "git cl try"?')
2172 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002173 print('Tried jobs on:')
2174 length = max(len(builder) for builder in builders_and_tests)
2175 for builder in sorted(builders_and_tests):
2176 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002177 return 0
2178
2179
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002180@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002181def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002182 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002183 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002184 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002185 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002186
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002188 if args:
2189 # One arg means set upstream branch.
2190 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2191 cl = Changelist()
2192 print "Upstream branch set to " + cl.GetUpstreamBranch()
2193 else:
2194 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195 return 0
2196
2197
thestig@chromium.org00858c82013-12-02 23:08:03 +00002198def CMDweb(parser, args):
2199 """Opens the current CL in the web browser."""
2200 _, args = parser.parse_args(args)
2201 if args:
2202 parser.error('Unrecognized args: %s' % ' '.join(args))
2203
2204 issue_url = Changelist().GetIssueURL()
2205 if not issue_url:
2206 print >> sys.stderr, 'ERROR No issue to open'
2207 return 1
2208
2209 webbrowser.open(issue_url)
2210 return 0
2211
2212
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002213def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002214 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002215 _, args = parser.parse_args(args)
2216 if args:
2217 parser.error('Unrecognized args: %s' % ' '.join(args))
2218 cl = Changelist()
2219 cl.SetFlag('commit', '1')
2220 return 0
2221
2222
groby@chromium.org411034a2013-02-26 15:12:01 +00002223def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002224 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002225 _, args = parser.parse_args(args)
2226 if args:
2227 parser.error('Unrecognized args: %s' % ' '.join(args))
2228 cl = Changelist()
2229 # Ensure there actually is an issue to close.
2230 cl.GetDescription()
2231 cl.CloseIssue()
2232 return 0
2233
2234
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002235def CMDdiff(parser, args):
2236 """shows differences between local tree and last upload."""
2237 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002238 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002239 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002240 if not issue:
2241 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002242 TMP_BRANCH = 'git-cl-diff'
2243 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2244
2245 # Create a new branch based on the merge-base
2246 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2247 try:
2248 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002249 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002250 if rtn != 0:
2251 return rtn
2252
2253 # Switch back to starting brand and diff against the temporary
2254 # branch containing the latest rietveld patch.
2255 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2256 finally:
2257 RunGit(['checkout', '-q', branch])
2258 RunGit(['branch', '-D', TMP_BRANCH])
2259
2260 return 0
2261
2262
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002263def CMDowners(parser, args):
2264 """interactively find the owners for reviewing"""
2265 parser.add_option(
2266 '--no-color',
2267 action='store_true',
2268 help='Use this option to disable color output')
2269 options, args = parser.parse_args(args)
2270
2271 author = RunGit(['config', 'user.email']).strip() or None
2272
2273 cl = Changelist()
2274
2275 if args:
2276 if len(args) > 1:
2277 parser.error('Unknown args')
2278 base_branch = args[0]
2279 else:
2280 # Default to diffing against the common ancestor of the upstream branch.
2281 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2282
2283 change = cl.GetChange(base_branch, None)
2284 return owners_finder.OwnersFinder(
2285 [f.LocalPath() for f in
2286 cl.GetChange(base_branch, None).AffectedFiles()],
2287 change.RepositoryRoot(), author,
2288 fopen=file, os_path=os.path, glob=glob.glob,
2289 disable_color=options.no_color).run()
2290
2291
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002292def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002293 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002294 CLANG_EXTS = ['.cc', '.cpp', '.h']
2295 parser.add_option('--full', action='store_true', default=False)
2296 opts, args = parser.parse_args(args)
2297 if args:
2298 parser.error('Unrecognized args: %s' % ' '.join(args))
2299
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002300 # git diff generates paths against the root of the repository. Change
2301 # to that directory so clang-format can find files even within subdirs.
2302 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2303 if rel_base_path:
2304 os.chdir(rel_base_path)
2305
digit@chromium.org29e47272013-05-17 17:01:46 +00002306 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002307 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002308 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002309 # Only list the names of modified files.
2310 diff_cmd.append('--name-only')
2311 else:
2312 # Only generate context-less patches.
2313 diff_cmd.append('-U0')
2314
2315 # Grab the merge-base commit, i.e. the upstream commit of the current
2316 # branch when it was created or the last time it was rebased. This is
2317 # to cover the case where the user may have called "git fetch origin",
2318 # moving the origin branch to a newer commit, but hasn't rebased yet.
2319 upstream_commit = None
2320 cl = Changelist()
2321 upstream_branch = cl.GetUpstreamBranch()
2322 if upstream_branch:
2323 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2324 upstream_commit = upstream_commit.strip()
2325
2326 if not upstream_commit:
2327 DieWithError('Could not find base commit for this branch. '
2328 'Are you in detached state?')
2329
2330 diff_cmd.append(upstream_commit)
2331
2332 # Handle source file filtering.
2333 diff_cmd.append('--')
2334 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2335 diff_output = RunGit(diff_cmd)
2336
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002337 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2338
digit@chromium.org29e47272013-05-17 17:01:46 +00002339 if opts.full:
2340 # diff_output is a list of files to send to clang-format.
2341 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002342 if not files:
2343 print "Nothing to format."
2344 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002345 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2346 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002347 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002348 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002349 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2350 'clang-format-diff.py')
2351 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002352 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002353 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002354
2355 # Newer versions of clang-format-diff.py require an explicit -i flag
2356 # to apply the edits to files, otherwise it just displays a diff.
2357 # Probe the usage string to verify if this is needed.
2358 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2359 if '[-i]' in help_text:
2360 cmd.append('-i')
2361
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002362 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002363
2364 return 0
2365
2366
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002367class OptionParser(optparse.OptionParser):
2368 """Creates the option parse and add --verbose support."""
2369 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002370 optparse.OptionParser.__init__(
2371 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002372 self.add_option(
2373 '-v', '--verbose', action='count', default=0,
2374 help='Use 2 times for more debugging info')
2375
2376 def parse_args(self, args=None, values=None):
2377 options, args = optparse.OptionParser.parse_args(self, args, values)
2378 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2379 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2380 return options, args
2381
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002382
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002383def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002384 if sys.hexversion < 0x02060000:
2385 print >> sys.stderr, (
2386 '\nYour python version %s is unsupported, please upgrade.\n' %
2387 sys.version.split(' ', 1)[0])
2388 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002389
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002390 # Reload settings.
2391 global settings
2392 settings = Settings()
2393
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002394 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002395 dispatcher = subcommand.CommandDispatcher(__name__)
2396 try:
2397 return dispatcher.execute(OptionParser(), argv)
2398 except urllib2.HTTPError, e:
2399 if e.code != 500:
2400 raise
2401 DieWithError(
2402 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2403 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002404
2405
2406if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002407 # These affect sys.stdout so do it outside of main() to simplify mocks in
2408 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002409 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002410 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002411 sys.exit(main(sys.argv[1:]))