blob: fa04059d9b5eb1d72eeeb11eba14e18af6b3e0d9 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000035import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000036import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000038import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000040import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000041import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000043import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000047DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000048POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000050GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000051CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000053# Shortcut since it quickly becomes redundant.
54Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000055
maruel@chromium.orgddd59412011-11-30 14:20:38 +000056# Initialized in main()
57settings = None
58
59
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000061 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062 sys.exit(1)
63
64
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000067 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000068 except subprocess2.CalledProcessError as e:
69 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 'Command "%s" failed.\n%s' % (
73 ' '.join(args), error_message or e.stdout or ''))
74 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
76
77def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000079 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000084 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000085 env = os.environ.copy()
86 # 'cat' is a magical git string that disables pagers on all platforms.
87 env['GIT_PAGER'] = 'cat'
88 out, code = subprocess2.communicate(['git'] + args,
89 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000090 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000091 return code, out[0]
92 except ValueError:
93 # When the subprocess fails, it returns None. That triggers a ValueError
94 # when trying to unpack the return value into (out, code).
95 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096
97
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000098def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000099 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000100 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000101 return (version.startswith(prefix) and
102 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000103
104
maruel@chromium.org90541732011-04-01 17:54:18 +0000105def ask_for_data(prompt):
106 try:
107 return raw_input(prompt)
108 except KeyboardInterrupt:
109 # Hide the exception.
110 sys.exit(1)
111
112
iannucci@chromium.org79540052012-10-19 23:15:26 +0000113def git_set_branch_value(key, value):
114 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000115 if not branch:
116 return
117
118 cmd = ['config']
119 if isinstance(value, int):
120 cmd.append('--int')
121 git_key = 'branch.%s.%s' % (branch, key)
122 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000123
124
125def git_get_branch_default(key, default):
126 branch = Changelist().GetBranch()
127 if branch:
128 git_key = 'branch.%s.%s' % (branch, key)
129 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
130 try:
131 return int(stdout.strip())
132 except ValueError:
133 pass
134 return default
135
136
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137def add_git_similarity(parser):
138 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000140 help='Sets the percentage that a pair of files need to match in order to'
141 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 parser.add_option(
143 '--find-copies', action='store_true',
144 help='Allows git to look for copies.')
145 parser.add_option(
146 '--no-find-copies', action='store_false', dest='find_copies',
147 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000148
149 old_parser_args = parser.parse_args
150 def Parse(args):
151 options, args = old_parser_args(args)
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156 print('Note: Saving similarity of %d%% in git config.'
157 % options.similarity)
158 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160 options.similarity = max(0, min(options.similarity, 100))
161
162 if options.find_copies is None:
163 options.find_copies = bool(
164 git_get_branch_default('git-find-copies', True))
165 else:
166 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000167
168 print('Using %d%% similarity for rename/copy detection. '
169 'Override with --similarity.' % options.similarity)
170
171 return options, args
172 parser.parse_args = Parse
173
174
ukai@chromium.org259e4682012-10-25 07:36:33 +0000175def is_dirty_git_tree(cmd):
176 # Make sure index is up-to-date before running diff-index.
177 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
178 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
179 if dirty:
180 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
181 print 'Uncommitted files: (git diff-index --name-status HEAD)'
182 print dirty[:4096]
183 if len(dirty) > 4096:
184 print '... (run "git diff-index --name-status HEAD" to see full output).'
185 return True
186 return False
187
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000188
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000189def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
190 """Return the corresponding git ref if |base_url| together with |glob_spec|
191 matches the full |url|.
192
193 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
194 """
195 fetch_suburl, as_ref = glob_spec.split(':')
196 if allow_wildcards:
197 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
198 if glob_match:
199 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
200 # "branches/{472,597,648}/src:refs/remotes/svn/*".
201 branch_re = re.escape(base_url)
202 if glob_match.group(1):
203 branch_re += '/' + re.escape(glob_match.group(1))
204 wildcard = glob_match.group(2)
205 if wildcard == '*':
206 branch_re += '([^/]*)'
207 else:
208 # Escape and replace surrounding braces with parentheses and commas
209 # with pipe symbols.
210 wildcard = re.escape(wildcard)
211 wildcard = re.sub('^\\\\{', '(', wildcard)
212 wildcard = re.sub('\\\\,', '|', wildcard)
213 wildcard = re.sub('\\\\}$', ')', wildcard)
214 branch_re += wildcard
215 if glob_match.group(3):
216 branch_re += re.escape(glob_match.group(3))
217 match = re.match(branch_re, url)
218 if match:
219 return re.sub('\*$', match.group(1), as_ref)
220
221 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
222 if fetch_suburl:
223 full_url = base_url + '/' + fetch_suburl
224 else:
225 full_url = base_url
226 if full_url == url:
227 return as_ref
228 return None
229
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000230
iannucci@chromium.org79540052012-10-19 23:15:26 +0000231def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000232 """Prints statistics about the change to the user."""
233 # --no-ext-diff is broken in some versions of Git, so try to work around
234 # this by overriding the environment (but there is still a problem if the
235 # git config key "diff.external" is used).
236 env = os.environ.copy()
237 if 'GIT_EXTERNAL_DIFF' in env:
238 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000239 # 'cat' is a magical git string that disables pagers on all platforms.
240 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241
242 if find_copies:
243 similarity_options = ['--find-copies-harder', '-l100000',
244 '-C%s' % similarity]
245 else:
246 similarity_options = ['-M%s' % similarity]
247
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000249 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000250 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252
253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000254class Settings(object):
255 def __init__(self):
256 self.default_server = None
257 self.cc = None
258 self.root = None
259 self.is_git_svn = None
260 self.svn_branch = None
261 self.tree_status_url = None
262 self.viewvc_url = None
263 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000264 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000265 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266
267 def LazyUpdateIfNeeded(self):
268 """Updates the settings from a codereview.settings file, if available."""
269 if not self.updated:
270 cr_settings_file = FindCodereviewSettingsFile()
271 if cr_settings_file:
272 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000273 self.updated = True
274 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 self.updated = True
276
277 def GetDefaultServerUrl(self, error_ok=False):
278 if not self.default_server:
279 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000280 self.default_server = gclient_utils.UpgradeToHttps(
281 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282 if error_ok:
283 return self.default_server
284 if not self.default_server:
285 error_message = ('Could not find settings file. You must configure '
286 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000287 self.default_server = gclient_utils.UpgradeToHttps(
288 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289 return self.default_server
290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000291 def GetRoot(self):
292 if not self.root:
293 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
294 return self.root
295
296 def GetIsGitSvn(self):
297 """Return true if this repo looks like it's using git-svn."""
298 if self.is_git_svn is None:
299 # If you have any "svn-remote.*" config keys, we think you're using svn.
300 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000301 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 return self.is_git_svn
303
304 def GetSVNBranch(self):
305 if self.svn_branch is None:
306 if not self.GetIsGitSvn():
307 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
308
309 # Try to figure out which remote branch we're based on.
310 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000311 # 1) iterate through our branch history and find the svn URL.
312 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000313
314 # regexp matching the git-svn line that contains the URL.
315 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
316
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000317 env = os.environ.copy()
318 # 'cat' is a magical git string that disables pagers on all platforms.
319 env['GIT_PAGER'] = 'cat'
320
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000321 # We don't want to go through all of history, so read a line from the
322 # pipe at a time.
323 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000324 cmd = ['git', 'log', '-100', '--pretty=medium']
325 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000326 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000327 for line in proc.stdout:
328 match = git_svn_re.match(line)
329 if match:
330 url = match.group(1)
331 proc.stdout.close() # Cut pipe.
332 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000334 if url:
335 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
336 remotes = RunGit(['config', '--get-regexp',
337 r'^svn-remote\..*\.url']).splitlines()
338 for remote in remotes:
339 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000340 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000341 remote = match.group(1)
342 base_url = match.group(2)
343 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000344 ['config', 'svn-remote.%s.fetch' % remote],
345 error_ok=True).strip()
346 if fetch_spec:
347 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
348 if self.svn_branch:
349 break
350 branch_spec = RunGit(
351 ['config', 'svn-remote.%s.branches' % remote],
352 error_ok=True).strip()
353 if branch_spec:
354 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
355 if self.svn_branch:
356 break
357 tag_spec = RunGit(
358 ['config', 'svn-remote.%s.tags' % remote],
359 error_ok=True).strip()
360 if tag_spec:
361 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
362 if self.svn_branch:
363 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000364
365 if not self.svn_branch:
366 DieWithError('Can\'t guess svn branch -- try specifying it on the '
367 'command line')
368
369 return self.svn_branch
370
371 def GetTreeStatusUrl(self, error_ok=False):
372 if not self.tree_status_url:
373 error_message = ('You must configure your tree status URL by running '
374 '"git cl config".')
375 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
376 error_ok=error_ok,
377 error_message=error_message)
378 return self.tree_status_url
379
380 def GetViewVCUrl(self):
381 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000382 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000383 return self.viewvc_url
384
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000385 def GetDefaultCCList(self):
386 return self._GetConfig('rietveld.cc', error_ok=True)
387
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000388 def GetDefaultPrivateFlag(self):
389 return self._GetConfig('rietveld.private', error_ok=True)
390
ukai@chromium.orge8077812012-02-03 03:41:46 +0000391 def GetIsGerrit(self):
392 """Return true if this repo is assosiated with gerrit code review system."""
393 if self.is_gerrit is None:
394 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
395 return self.is_gerrit
396
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000397 def GetGitEditor(self):
398 """Return the editor specified in the git config, or None if none is."""
399 if self.git_editor is None:
400 self.git_editor = self._GetConfig('core.editor', error_ok=True)
401 return self.git_editor or None
402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403 def _GetConfig(self, param, **kwargs):
404 self.LazyUpdateIfNeeded()
405 return RunGit(['config', param], **kwargs).strip()
406
407
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000408def ShortBranchName(branch):
409 """Convert a name like 'refs/heads/foo' to just 'foo'."""
410 return branch.replace('refs/heads/', '')
411
412
413class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000414 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000416 global settings
417 if not settings:
418 # Happens when git_cl.py is used as a utility library.
419 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000420 settings.GetDefaultServerUrl()
421 self.branchref = branchref
422 if self.branchref:
423 self.branch = ShortBranchName(self.branchref)
424 else:
425 self.branch = None
426 self.rietveld_server = None
427 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000428 self.lookedup_issue = False
429 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 self.has_description = False
431 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000432 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000434 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000435 self.cc = None
436 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000437 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000438 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000439
440 def GetCCList(self):
441 """Return the users cc'd on this CL.
442
443 Return is a string suitable for passing to gcl with the --cc flag.
444 """
445 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000446 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000447 more_cc = ','.join(self.watchers)
448 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
449 return self.cc
450
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000451 def GetCCListWithoutDefault(self):
452 """Return the users cc'd on this CL excluding default ones."""
453 if self.cc is None:
454 self.cc = ','.join(self.watchers)
455 return self.cc
456
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000457 def SetWatchers(self, watchers):
458 """Set the list of email addresses that should be cc'd based on the changed
459 files in this CL.
460 """
461 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000462
463 def GetBranch(self):
464 """Returns the short branch name, e.g. 'master'."""
465 if not self.branch:
466 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
467 self.branch = ShortBranchName(self.branchref)
468 return self.branch
469
470 def GetBranchRef(self):
471 """Returns the full branch name, e.g. 'refs/heads/master'."""
472 self.GetBranch() # Poke the lazy loader.
473 return self.branchref
474
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000475 @staticmethod
476 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000477 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 e.g. 'origin', 'refs/heads/master'
479 """
480 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
482 error_ok=True).strip()
483 if upstream_branch:
484 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
485 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000486 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
487 error_ok=True).strip()
488 if upstream_branch:
489 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000490 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000491 # Fall back on trying a git-svn upstream branch.
492 if settings.GetIsGitSvn():
493 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000495 # Else, try to guess the origin remote.
496 remote_branches = RunGit(['branch', '-r']).split()
497 if 'origin/master' in remote_branches:
498 # Fall back on origin/master if it exits.
499 remote = 'origin'
500 upstream_branch = 'refs/heads/master'
501 elif 'origin/trunk' in remote_branches:
502 # Fall back on origin/trunk if it exists. Generally a shared
503 # git-svn clone
504 remote = 'origin'
505 upstream_branch = 'refs/heads/trunk'
506 else:
507 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000508Either pass complete "git diff"-style arguments, like
509 git cl upload origin/master
510or verify this branch is set up to track another (via the --track argument to
511"git checkout -b ...").""")
512
513 return remote, upstream_branch
514
515 def GetUpstreamBranch(self):
516 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000517 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000518 if remote is not '.':
519 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
520 self.upstream_branch = upstream_branch
521 return self.upstream_branch
522
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000523 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000524 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000525 remote, branch = None, self.GetBranch()
526 seen_branches = set()
527 while branch not in seen_branches:
528 seen_branches.add(branch)
529 remote, branch = self.FetchUpstreamTuple(branch)
530 branch = ShortBranchName(branch)
531 if remote != '.' or branch.startswith('refs/remotes'):
532 break
533 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000534 remotes = RunGit(['remote'], error_ok=True).split()
535 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000537 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000538 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000539 logging.warning('Could not determine which remote this change is '
540 'associated with, so defaulting to "%s". This may '
541 'not be what you want. You may prevent this message '
542 'by running "git svn info" as documented here: %s',
543 self._remote,
544 GIT_INSTRUCTIONS_URL)
545 else:
546 logging.warn('Could not determine which remote this change is '
547 'associated with. You may prevent this message by '
548 'running "git svn info" as documented here: %s',
549 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000550 branch = 'HEAD'
551 if branch.startswith('refs/remotes'):
552 self._remote = (remote, branch)
553 else:
554 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000555 return self._remote
556
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000557 def GitSanityChecks(self, upstream_git_obj):
558 """Checks git repo status and ensures diff is from local commits."""
559
560 # Verify the commit we're diffing against is in our current branch.
561 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
562 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
563 if upstream_sha != common_ancestor:
564 print >> sys.stderr, (
565 'ERROR: %s is not in the current branch. You may need to rebase '
566 'your tracking branch' % upstream_sha)
567 return False
568
569 # List the commits inside the diff, and verify they are all local.
570 commits_in_diff = RunGit(
571 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
572 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
573 remote_branch = remote_branch.strip()
574 if code != 0:
575 _, remote_branch = self.GetRemoteBranch()
576
577 commits_in_remote = RunGit(
578 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
579
580 common_commits = set(commits_in_diff) & set(commits_in_remote)
581 if common_commits:
582 print >> sys.stderr, (
583 'ERROR: Your diff contains %d commits already in %s.\n'
584 'Run "git log --oneline %s..HEAD" to get a list of commits in '
585 'the diff. If you are using a custom git flow, you can override'
586 ' the reference used for this check with "git config '
587 'gitcl.remotebranch <git-ref>".' % (
588 len(common_commits), remote_branch, upstream_git_obj))
589 return False
590 return True
591
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000592 def GetGitBaseUrlFromConfig(self):
593 """Return the configured base URL from branch.<branchname>.baseurl.
594
595 Returns None if it is not set.
596 """
597 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
598 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 def GetRemoteUrl(self):
601 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
602
603 Returns None if there is no remote.
604 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000605 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
607
608 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000609 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000610 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000612 self.issue = int(issue) or None if issue else None
613 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 return self.issue
615
616 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000617 if not self.rietveld_server:
618 # If we're on a branch then get the server potentially associated
619 # with that branch.
620 if self.GetIssue():
621 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
622 ['config', self._RietveldServer()], error_ok=True).strip())
623 if not self.rietveld_server:
624 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 return self.rietveld_server
626
627 def GetIssueURL(self):
628 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000629 if not self.GetIssue():
630 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
632
633 def GetDescription(self, pretty=False):
634 if not self.has_description:
635 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000636 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000637 try:
638 self.description = self.RpcServer().get_description(issue).strip()
639 except urllib2.HTTPError, e:
640 if e.code == 404:
641 DieWithError(
642 ('\nWhile fetching the description for issue %d, received a '
643 '404 (not found)\n'
644 'error. It is likely that you deleted this '
645 'issue on the server. If this is the\n'
646 'case, please run\n\n'
647 ' git cl issue 0\n\n'
648 'to clear the association with the deleted issue. Then run '
649 'this command again.') % issue)
650 else:
651 DieWithError(
652 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653 self.has_description = True
654 if pretty:
655 wrapper = textwrap.TextWrapper()
656 wrapper.initial_indent = wrapper.subsequent_indent = ' '
657 return wrapper.fill(self.description)
658 return self.description
659
660 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000661 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000662 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000663 patchset = RunGit(['config', self._PatchsetSetting()],
664 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000665 self.patchset = int(patchset) or None if patchset else None
666 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667 return self.patchset
668
669 def SetPatchset(self, patchset):
670 """Set this branch's patchset. If patchset=0, clears the patchset."""
671 if patchset:
672 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000673 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674 else:
675 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000676 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000677 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000679 def GetMostRecentPatchset(self):
680 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000681
682 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000683 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000684 '/download/issue%s_%s.diff' % (issue, patchset))
685
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000686 def GetIssueProperties(self):
687 if self._props is None:
688 issue = self.GetIssue()
689 if not issue:
690 self._props = {}
691 else:
692 self._props = self.RpcServer().get_issue_properties(issue, True)
693 return self._props
694
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000695 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000696 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698 def SetIssue(self, issue):
699 """Set this branch's issue. If issue=0, clears the issue."""
700 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000701 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702 RunGit(['config', self._IssueSetting(), str(issue)])
703 if self.rietveld_server:
704 RunGit(['config', self._RietveldServer(), self.rietveld_server])
705 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000706 current_issue = self.GetIssue()
707 if current_issue:
708 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000709 self.issue = None
710 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000712 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000713 if not self.GitSanityChecks(upstream_branch):
714 DieWithError('\nGit sanity check failure')
715
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000716 env = os.environ.copy()
717 # 'cat' is a magical git string that disables pagers on all platforms.
718 env['GIT_PAGER'] = 'cat'
719
720 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000721 if not root:
722 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000723 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000724
725 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000726 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000727 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000728 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000729 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000730 except subprocess2.CalledProcessError:
731 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000732 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000733 'This branch probably doesn\'t exist anymore. To reset the\n'
734 'tracking branch, please run\n'
735 ' git branch --set-upstream %s trunk\n'
736 'replacing trunk with origin/master or the relevant branch') %
737 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000738
maruel@chromium.org52424302012-08-29 15:14:30 +0000739 issue = self.GetIssue()
740 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000741 if issue:
742 description = self.GetDescription()
743 else:
744 # If the change was never uploaded, use the log messages of all commits
745 # up to the branch point, as git cl upload will prefill the description
746 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000747 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000748 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000749 '%s...' % (upstream_branch)],
750 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000751
752 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000753 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000754 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000755 name,
756 description,
757 absroot,
758 files,
759 issue,
760 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000761 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000762
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000763 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000764 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000765
766 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000767 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000768 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000769 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000770 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000771 except presubmit_support.PresubmitFailure, e:
772 DieWithError(
773 ('%s\nMaybe your depot_tools is out of date?\n'
774 'If all fails, contact maruel@') % e)
775
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000776 def UpdateDescription(self, description):
777 self.description = description
778 return self.RpcServer().update_description(
779 self.GetIssue(), self.description)
780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000782 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000783 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000785 def SetFlag(self, flag, value):
786 """Patchset must match."""
787 if not self.GetPatchset():
788 DieWithError('The patchset needs to match. Send another patchset.')
789 try:
790 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000791 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000792 except urllib2.HTTPError, e:
793 if e.code == 404:
794 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
795 if e.code == 403:
796 DieWithError(
797 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
798 'match?') % (self.GetIssue(), self.GetPatchset()))
799 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000801 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 """Returns an upload.RpcServer() to access this review's rietveld instance.
803 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000804 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000805 self._rpc_server = rietveld.CachingRietveld(
806 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000807 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808
809 def _IssueSetting(self):
810 """Return the git setting that stores this change's issue."""
811 return 'branch.%s.rietveldissue' % self.GetBranch()
812
813 def _PatchsetSetting(self):
814 """Return the git setting that stores this change's most recent patchset."""
815 return 'branch.%s.rietveldpatchset' % self.GetBranch()
816
817 def _RietveldServer(self):
818 """Returns the git setting that stores this change's rietveld server."""
819 return 'branch.%s.rietveldserver' % self.GetBranch()
820
821
822def GetCodereviewSettingsInteractively():
823 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000824 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 server = settings.GetDefaultServerUrl(error_ok=True)
826 prompt = 'Rietveld server (host[:port])'
827 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000828 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829 if not server and not newserver:
830 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000831 if newserver:
832 newserver = gclient_utils.UpgradeToHttps(newserver)
833 if newserver != server:
834 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000836 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 prompt = caption
838 if initial:
839 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000840 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 if new_val == 'x':
842 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000843 elif new_val:
844 if is_url:
845 new_val = gclient_utils.UpgradeToHttps(new_val)
846 if new_val != initial:
847 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000849 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000850 SetProperty(settings.GetDefaultPrivateFlag(),
851 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000853 'tree-status-url', False)
854 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855
856 # TODO: configure a default branch to diff against, rather than this
857 # svn-based hackery.
858
859
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000860class ChangeDescription(object):
861 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000862 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000863 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000864
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000865 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000866 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000867
agable@chromium.org42c20792013-09-12 17:34:49 +0000868 @property # www.logilab.org/ticket/89786
869 def description(self): # pylint: disable=E0202
870 return '\n'.join(self._description_lines)
871
872 def set_description(self, desc):
873 if isinstance(desc, basestring):
874 lines = desc.splitlines()
875 else:
876 lines = [line.rstrip() for line in desc]
877 while lines and not lines[0]:
878 lines.pop(0)
879 while lines and not lines[-1]:
880 lines.pop(-1)
881 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882
883 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000884 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000885 assert isinstance(reviewers, list), reviewers
886 if not reviewers:
887 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000888 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000889
agable@chromium.org42c20792013-09-12 17:34:49 +0000890 # Get the set of R= and TBR= lines and remove them from the desciption.
891 regexp = re.compile(self.R_LINE)
892 matches = [regexp.match(line) for line in self._description_lines]
893 new_desc = [l for i, l in enumerate(self._description_lines)
894 if not matches[i]]
895 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000896
agable@chromium.org42c20792013-09-12 17:34:49 +0000897 # Construct new unified R= and TBR= lines.
898 r_names = []
899 tbr_names = []
900 for match in matches:
901 if not match:
902 continue
903 people = cleanup_list([match.group(2).strip()])
904 if match.group(1) == 'TBR':
905 tbr_names.extend(people)
906 else:
907 r_names.extend(people)
908 for name in r_names:
909 if name not in reviewers:
910 reviewers.append(name)
911 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
912 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
913
914 # Put the new lines in the description where the old first R= line was.
915 line_loc = next((i for i, match in enumerate(matches) if match), -1)
916 if 0 <= line_loc < len(self._description_lines):
917 if new_tbr_line:
918 self._description_lines.insert(line_loc, new_tbr_line)
919 if new_r_line:
920 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000921 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000922 if new_r_line:
923 self.append_footer(new_r_line)
924 if new_tbr_line:
925 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000926
927 def prompt(self):
928 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000929 self.set_description([
930 '# Enter a description of the change.',
931 '# This will be displayed on the codereview site.',
932 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000933 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000934 '--------------------',
935 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000936
agable@chromium.org42c20792013-09-12 17:34:49 +0000937 regexp = re.compile(self.BUG_LINE)
938 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000939 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000940 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000941 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000942 if not content:
943 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000944 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000945
946 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
948 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000949 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000950 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000951
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000952 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000953 if self._description_lines:
954 # Add an empty line if either the last line or the new line isn't a tag.
955 last_line = self._description_lines[-1]
956 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
957 not presubmit_support.Change.TAG_LINE_RE.match(line)):
958 self._description_lines.append('')
959 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000960
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000961 def get_reviewers(self):
962 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
964 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000965 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000966
967
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000968def get_approving_reviewers(props):
969 """Retrieves the reviewers that approved a CL from the issue properties with
970 messages.
971
972 Note that the list may contain reviewers that are not committer, thus are not
973 considered by the CQ.
974 """
975 return sorted(
976 set(
977 message['sender']
978 for message in props['messages']
979 if message['approval'] and message['sender'] in props['reviewers']
980 )
981 )
982
983
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984def FindCodereviewSettingsFile(filename='codereview.settings'):
985 """Finds the given file starting in the cwd and going up.
986
987 Only looks up to the top of the repository unless an
988 'inherit-review-settings-ok' file exists in the root of the repository.
989 """
990 inherit_ok_file = 'inherit-review-settings-ok'
991 cwd = os.getcwd()
992 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
993 if os.path.isfile(os.path.join(root, inherit_ok_file)):
994 root = '/'
995 while True:
996 if filename in os.listdir(cwd):
997 if os.path.isfile(os.path.join(cwd, filename)):
998 return open(os.path.join(cwd, filename))
999 if cwd == root:
1000 break
1001 cwd = os.path.dirname(cwd)
1002
1003
1004def LoadCodereviewSettingsFromFile(fileobj):
1005 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001006 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 def SetProperty(name, setting, unset_error_ok=False):
1009 fullname = 'rietveld.' + name
1010 if setting in keyvals:
1011 RunGit(['config', fullname, keyvals[setting]])
1012 else:
1013 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1014
1015 SetProperty('server', 'CODE_REVIEW_SERVER')
1016 # Only server setting is required. Other settings can be absent.
1017 # In that case, we ignore errors raised during option deletion attempt.
1018 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001019 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1021 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1022
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001023 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001024 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1027 #should be of the form
1028 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1029 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1030 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1031 keyvals['ORIGIN_URL_CONFIG']])
1032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001034def urlretrieve(source, destination):
1035 """urllib is broken for SSL connections via a proxy therefore we
1036 can't use urllib.urlretrieve()."""
1037 with open(destination, 'w') as f:
1038 f.write(urllib2.urlopen(source).read())
1039
1040
ukai@chromium.org712d6102013-11-27 00:52:58 +00001041def hasSheBang(fname):
1042 """Checks fname is a #! script."""
1043 with open(fname) as f:
1044 return f.read(2).startswith('#!')
1045
1046
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001047def DownloadHooks(force):
1048 """downloads hooks
1049
1050 Args:
1051 force: True to update hooks. False to install hooks if not present.
1052 """
1053 if not settings.GetIsGerrit():
1054 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001055 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001056 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1057 if not os.access(dst, os.X_OK):
1058 if os.path.exists(dst):
1059 if not force:
1060 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001061 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001062 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001063 if not hasSheBang(dst):
1064 DieWithError('Not a script: %s\n'
1065 'You need to download from\n%s\n'
1066 'into .git/hooks/commit-msg and '
1067 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001068 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1069 except Exception:
1070 if os.path.exists(dst):
1071 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001072 DieWithError('\nFailed to download hooks.\n'
1073 'You need to download from\n%s\n'
1074 'into .git/hooks/commit-msg and '
1075 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001076
1077
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001078@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001079def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001080 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001082 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 if len(args) == 0:
1084 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001085 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086 return 0
1087
1088 url = args[0]
1089 if not url.endswith('codereview.settings'):
1090 url = os.path.join(url, 'codereview.settings')
1091
1092 # Load code review settings and download hooks (if available).
1093 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001094 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 return 0
1096
1097
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001098def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001099 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001100 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1101 branch = ShortBranchName(branchref)
1102 _, args = parser.parse_args(args)
1103 if not args:
1104 print("Current base-url:")
1105 return RunGit(['config', 'branch.%s.base-url' % branch],
1106 error_ok=False).strip()
1107 else:
1108 print("Setting base-url to %s" % args[0])
1109 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1110 error_ok=False).strip()
1111
1112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001114 """Show status of changelists.
1115
1116 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001117 - Red not sent for review or broken
1118 - Blue waiting for review
1119 - Yellow waiting for you to reply to review
1120 - Green LGTM'ed
1121 - Magenta in the commit queue
1122 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001123
1124 Also see 'git cl comments'.
1125 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 parser.add_option('--field',
1127 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001128 parser.add_option('-f', '--fast', action='store_true',
1129 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001131 if args:
1132 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001135 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136 if options.field.startswith('desc'):
1137 print cl.GetDescription()
1138 elif options.field == 'id':
1139 issueid = cl.GetIssue()
1140 if issueid:
1141 print issueid
1142 elif options.field == 'patch':
1143 patchset = cl.GetPatchset()
1144 if patchset:
1145 print patchset
1146 elif options.field == 'url':
1147 url = cl.GetIssueURL()
1148 if url:
1149 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001150 return 0
1151
1152 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1153 if not branches:
1154 print('No local branch found.')
1155 return 0
1156
1157 changes = (Changelist(branchref=b) for b in branches.splitlines())
1158 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1159 alignment = max(5, max(len(b) for b in branches))
1160 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001161 # Adhoc thread pool to request data concurrently.
1162 output = Queue.Queue()
1163
1164 # Silence upload.py otherwise it becomes unweldly.
1165 upload.verbosity = 0
1166
1167 if not options.fast:
1168 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001169 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001170 c = Changelist(branchref=b)
1171 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001172 props = {}
1173 r = None
1174 if i:
1175 try:
1176 props = c.GetIssueProperties()
1177 r = c.GetApprovingReviewers() if i else None
1178 except urllib2.HTTPError:
1179 # The issue probably doesn't exist anymore.
1180 i += ' (broken)'
1181
1182 msgs = props.get('messages') or []
1183
1184 if not i:
1185 color = Fore.WHITE
1186 elif props.get('closed'):
1187 # Issue is closed.
1188 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001189 elif props.get('commit'):
1190 # Issue is in the commit queue.
1191 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001192 elif r:
1193 # Was LGTM'ed.
1194 color = Fore.GREEN
1195 elif not msgs:
1196 # No message was sent.
1197 color = Fore.RED
1198 elif msgs[-1]['sender'] != props.get('owner_email'):
1199 color = Fore.YELLOW
1200 else:
1201 color = Fore.BLUE
1202 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001203
1204 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1205 for t in threads:
1206 t.daemon = True
1207 t.start()
1208 else:
1209 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1210 for b in branches:
1211 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001212 url = c.GetIssueURL()
1213 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001214
1215 tmp = {}
1216 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001217 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001218 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001219 b, i, color = output.get()
1220 tmp[b] = (i, color)
1221 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001222 reset = Fore.RESET
1223 if not sys.stdout.isatty():
1224 color = ''
1225 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001226 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001227 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001228
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001229 cl = Changelist()
1230 print
1231 print 'Current branch:',
1232 if not cl.GetIssue():
1233 print 'no issue assigned.'
1234 return 0
1235 print cl.GetBranch()
1236 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1237 print 'Issue description:'
1238 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 return 0
1240
1241
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001242def colorize_CMDstatus_doc():
1243 """To be called once in main() to add colors to git cl status help."""
1244 colors = [i for i in dir(Fore) if i[0].isupper()]
1245
1246 def colorize_line(line):
1247 for color in colors:
1248 if color in line.upper():
1249 # Extract whitespaces first and the leading '-'.
1250 indent = len(line) - len(line.lstrip(' ')) + 1
1251 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1252 return line
1253
1254 lines = CMDstatus.__doc__.splitlines()
1255 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1256
1257
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001258@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001260 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261
1262 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001263 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001264 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265
1266 cl = Changelist()
1267 if len(args) > 0:
1268 try:
1269 issue = int(args[0])
1270 except ValueError:
1271 DieWithError('Pass a number to set the issue or none to list it.\n'
1272 'Maybe you want to run git cl status?')
1273 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001274 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275 return 0
1276
1277
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001278def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001279 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001280 (_, args) = parser.parse_args(args)
1281 if args:
1282 parser.error('Unsupported argument: %s' % args)
1283
1284 cl = Changelist()
1285 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001286 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001287 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001288 if message['disapproval']:
1289 color = Fore.RED
1290 elif message['approval']:
1291 color = Fore.GREEN
1292 elif message['sender'] == data['owner_email']:
1293 color = Fore.MAGENTA
1294 else:
1295 color = Fore.BLUE
1296 print '\n%s%s %s%s' % (
1297 color, message['date'].split('.', 1)[0], message['sender'],
1298 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001299 if message['text'].strip():
1300 print '\n'.join(' ' + l for l in message['text'].splitlines())
1301 return 0
1302
1303
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001304def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001305 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001306 cl = Changelist()
1307 if not cl.GetIssue():
1308 DieWithError('This branch has no associated changelist.')
1309 description = ChangeDescription(cl.GetDescription())
1310 description.prompt()
1311 cl.UpdateDescription(description.description)
1312 return 0
1313
1314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315def CreateDescriptionFromLog(args):
1316 """Pulls out the commit log to use as a base for the CL description."""
1317 log_args = []
1318 if len(args) == 1 and not args[0].endswith('.'):
1319 log_args = [args[0] + '..']
1320 elif len(args) == 1 and args[0].endswith('...'):
1321 log_args = [args[0][:-1]]
1322 elif len(args) == 2:
1323 log_args = [args[0] + '..' + args[1]]
1324 else:
1325 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001326 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327
1328
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001330 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001331 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001333 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001334 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335 (options, args) = parser.parse_args(args)
1336
ukai@chromium.org259e4682012-10-25 07:36:33 +00001337 if not options.force and is_dirty_git_tree('presubmit'):
1338 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 return 1
1340
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001341 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342 if args:
1343 base_branch = args[0]
1344 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001345 # Default to diffing against the common ancestor of the upstream branch.
1346 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001348 cl.RunHook(
1349 committing=not options.upload,
1350 may_prompt=False,
1351 verbose=options.verbose,
1352 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001353 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354
1355
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001356def AddChangeIdToCommitMessage(options, args):
1357 """Re-commits using the current message, assumes the commit hook is in
1358 place.
1359 """
1360 log_desc = options.message or CreateDescriptionFromLog(args)
1361 git_command = ['commit', '--amend', '-m', log_desc]
1362 RunGit(git_command)
1363 new_log_desc = CreateDescriptionFromLog(args)
1364 if CHANGE_ID in new_log_desc:
1365 print 'git-cl: Added Change-Id to commit message.'
1366 else:
1367 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1368
1369
ukai@chromium.orge8077812012-02-03 03:41:46 +00001370def GerritUpload(options, args, cl):
1371 """upload the current branch to gerrit."""
1372 # We assume the remote called "origin" is the one we want.
1373 # It is probably not worthwhile to support different workflows.
1374 remote = 'origin'
1375 branch = 'master'
1376 if options.target_branch:
1377 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001379 change_desc = ChangeDescription(
1380 options.message or CreateDescriptionFromLog(args))
1381 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001382 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001384 if CHANGE_ID not in change_desc.description:
1385 AddChangeIdToCommitMessage(options, args)
1386 if options.reviewers:
1387 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388
ukai@chromium.orge8077812012-02-03 03:41:46 +00001389 receive_options = []
1390 cc = cl.GetCCList().split(',')
1391 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001392 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001393 cc = filter(None, cc)
1394 if cc:
1395 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001396 if change_desc.get_reviewers():
1397 receive_options.extend(
1398 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399
ukai@chromium.orge8077812012-02-03 03:41:46 +00001400 git_command = ['push']
1401 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001402 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001403 ' '.join(receive_options))
1404 git_command += [remote, 'HEAD:refs/for/' + branch]
1405 RunGit(git_command)
1406 # TODO(ukai): parse Change-Id: and set issue number?
1407 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001408
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
ukai@chromium.orge8077812012-02-03 03:41:46 +00001410def RietveldUpload(options, args, cl):
1411 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1413 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 if options.emulate_svn_auto_props:
1415 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
1417 change_desc = None
1418
1419 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001420 if options.title:
1421 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001422 if options.message:
1423 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001424 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 print ("This branch is associated with issue %s. "
1426 "Adding patch to that issue." % cl.GetIssue())
1427 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001428 if options.title:
1429 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001430 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001431 change_desc = ChangeDescription(message)
1432 if options.reviewers:
1433 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001434 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001435 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001436
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001437 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 print "Description is empty; aborting."
1439 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001440
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001441 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001442 if change_desc.get_reviewers():
1443 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001444 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001445 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001446 DieWithError("Must specify reviewers to send email.")
1447 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001448
1449 # We check this before applying rietveld.private assuming that in
1450 # rietveld.cc only addresses which we can send private CLs to are listed
1451 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1452 # --private is specified explicitly on the command line.
1453 if options.private:
1454 logging.warn('rietveld.cc is ignored since private flag is specified. '
1455 'You need to review and add them manually if necessary.')
1456 cc = cl.GetCCListWithoutDefault()
1457 else:
1458 cc = cl.GetCCList()
1459 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001460 if cc:
1461 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001463 if options.private or settings.GetDefaultPrivateFlag() == "True":
1464 upload_args.append('--private')
1465
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001466 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001467 if not options.find_copies:
1468 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001469
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 # Include the upstream repo's URL in the change -- this is useful for
1471 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001472 remote_url = cl.GetGitBaseUrlFromConfig()
1473 if not remote_url:
1474 if settings.GetIsGitSvn():
1475 # URL is dependent on the current directory.
1476 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1477 if data:
1478 keys = dict(line.split(': ', 1) for line in data.splitlines()
1479 if ': ' in line)
1480 remote_url = keys.get('URL', None)
1481 else:
1482 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1483 remote_url = (cl.GetRemoteUrl() + '@'
1484 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485 if remote_url:
1486 upload_args.extend(['--base_url', remote_url])
1487
1488 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001489 upload_args = ['upload'] + upload_args + args
1490 logging.info('upload.RealMain(%s)', upload_args)
1491 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001492 issue = int(issue)
1493 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001494 except KeyboardInterrupt:
1495 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496 except:
1497 # If we got an exception after the user typed a description for their
1498 # change, back up the description before re-raising.
1499 if change_desc:
1500 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1501 print '\nGot exception while uploading -- saving description to %s\n' \
1502 % backup_path
1503 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001504 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505 backup_file.close()
1506 raise
1507
1508 if not cl.GetIssue():
1509 cl.SetIssue(issue)
1510 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001511
1512 if options.use_commit_queue:
1513 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001514 return 0
1515
1516
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001517def cleanup_list(l):
1518 """Fixes a list so that comma separated items are put as individual items.
1519
1520 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1521 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1522 """
1523 items = sum((i.split(',') for i in l), [])
1524 stripped_items = (i.strip() for i in items)
1525 return sorted(filter(None, stripped_items))
1526
1527
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001528@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001529def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001530 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001531 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1532 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001533 parser.add_option('--bypass-watchlists', action='store_true',
1534 dest='bypass_watchlists',
1535 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001536 parser.add_option('-f', action='store_true', dest='force',
1537 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001538 parser.add_option('-m', dest='message', help='message for patchset')
1539 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001540 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001541 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001542 help='reviewer email addresses')
1543 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001544 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001545 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001546 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547 help='send email to reviewer immediately')
1548 parser.add_option("--emulate_svn_auto_props", action="store_true",
1549 dest="emulate_svn_auto_props",
1550 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001551 parser.add_option('-c', '--use-commit-queue', action='store_true',
1552 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001553 parser.add_option('--private', action='store_true',
1554 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001555 parser.add_option('--target_branch',
1556 help='When uploading to gerrit, remote branch to '
1557 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001558 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001559 (options, args) = parser.parse_args(args)
1560
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001561 if options.target_branch and not settings.GetIsGerrit():
1562 parser.error('Use --target_branch for non gerrit repository.')
1563
ukai@chromium.org259e4682012-10-25 07:36:33 +00001564 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001565 return 1
1566
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001567 options.reviewers = cleanup_list(options.reviewers)
1568 options.cc = cleanup_list(options.cc)
1569
ukai@chromium.orge8077812012-02-03 03:41:46 +00001570 cl = Changelist()
1571 if args:
1572 # TODO(ukai): is it ok for gerrit case?
1573 base_branch = args[0]
1574 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001575 # Default to diffing against common ancestor of upstream branch
1576 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001577 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001578
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001579 # Apply watchlists on upload.
1580 change = cl.GetChange(base_branch, None)
1581 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1582 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001583 if not options.bypass_watchlists:
1584 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001585
ukai@chromium.orge8077812012-02-03 03:41:46 +00001586 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001587 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001588 may_prompt=not options.force,
1589 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001590 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001591 if not hook_results.should_continue():
1592 return 1
1593 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001594 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001595
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001596 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001597 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001598 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001599 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001600 print ('The last upload made from this repository was patchset #%d but '
1601 'the most recent patchset on the server is #%d.'
1602 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001603 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1604 'from another machine or branch the patch you\'re uploading now '
1605 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001606 ask_for_data('About to upload; enter to confirm.')
1607
iannucci@chromium.org79540052012-10-19 23:15:26 +00001608 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001609 if settings.GetIsGerrit():
1610 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001611 ret = RietveldUpload(options, args, cl)
1612 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001613 git_set_branch_value('last-upload-hash',
1614 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001615
1616 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001617
1618
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001619def IsSubmoduleMergeCommit(ref):
1620 # When submodules are added to the repo, we expect there to be a single
1621 # non-git-svn merge commit at remote HEAD with a signature comment.
1622 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001623 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001624 return RunGit(cmd) != ''
1625
1626
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001627def SendUpstream(parser, args, cmd):
1628 """Common code for CmdPush and CmdDCommit
1629
1630 Squashed commit into a single.
1631 Updates changelog with metadata (e.g. pointer to review).
1632 Pushes/dcommits the code upstream.
1633 Updates review and closes.
1634 """
1635 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1636 help='bypass upload presubmit hook')
1637 parser.add_option('-m', dest='message',
1638 help="override review description")
1639 parser.add_option('-f', action='store_true', dest='force',
1640 help="force yes to questions (don't prompt)")
1641 parser.add_option('-c', dest='contributor',
1642 help="external contributor for patch (appended to " +
1643 "description and used as author for git). Should be " +
1644 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001645 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646 (options, args) = parser.parse_args(args)
1647 cl = Changelist()
1648
1649 if not args or cmd == 'push':
1650 # Default to merging against our best guess of the upstream branch.
1651 args = [cl.GetUpstreamBranch()]
1652
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001653 if options.contributor:
1654 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1655 print "Please provide contibutor as 'First Last <email@example.com>'"
1656 return 1
1657
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001658 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001659 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001660
ukai@chromium.org259e4682012-10-25 07:36:33 +00001661 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662 return 1
1663
1664 # This rev-list syntax means "show all commits not in my branch that
1665 # are in base_branch".
1666 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1667 base_branch]).splitlines()
1668 if upstream_commits:
1669 print ('Base branch "%s" has %d commits '
1670 'not in this branch.' % (base_branch, len(upstream_commits)))
1671 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1672 return 1
1673
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001674 # This is the revision `svn dcommit` will commit on top of.
1675 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1676 '--pretty=format:%H'])
1677
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001678 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001679 # If the base_head is a submodule merge commit, the first parent of the
1680 # base_head should be a git-svn commit, which is what we're interested in.
1681 base_svn_head = base_branch
1682 if base_has_submodules:
1683 base_svn_head += '^1'
1684
1685 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686 if extra_commits:
1687 print ('This branch has %d additional commits not upstreamed yet.'
1688 % len(extra_commits.splitlines()))
1689 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1690 'before attempting to %s.' % (base_branch, cmd))
1691 return 1
1692
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001693 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001694 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001695 author = None
1696 if options.contributor:
1697 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001698 hook_results = cl.RunHook(
1699 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001700 may_prompt=not options.force,
1701 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001702 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001703 if not hook_results.should_continue():
1704 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001705
1706 if cmd == 'dcommit':
1707 # Check the tree status if the tree status URL is set.
1708 status = GetTreeStatus()
1709 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001710 print('The tree is closed. Please wait for it to reopen. Use '
1711 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001712 return 1
1713 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001714 print('Unable to determine tree status. Please verify manually and '
1715 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001716 else:
1717 breakpad.SendStack(
1718 'GitClHooksBypassedCommit',
1719 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001720 (cl.GetRietveldServer(), cl.GetIssue()),
1721 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001723 change_desc = ChangeDescription(options.message)
1724 if not change_desc.description and cl.GetIssue():
1725 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001727 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001728 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001729 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001730 else:
1731 print 'No description set.'
1732 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1733 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001734
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001735 # Keep a separate copy for the commit message, because the commit message
1736 # contains the link to the Rietveld issue, while the Rietveld message contains
1737 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001738 # Keep a separate copy for the commit message.
1739 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001740 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001741
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001742 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001743 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001744 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001746 commit_desc.append_footer('Patch from %s.' % options.contributor)
1747
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001748 print('Description:')
1749 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750
1751 branches = [base_branch, cl.GetBranchRef()]
1752 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001753 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001754 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001756 # We want to squash all this branch's commits into one commit with the proper
1757 # description. We do this by doing a "reset --soft" to the base branch (which
1758 # keeps the working copy the same), then dcommitting that. If origin/master
1759 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1760 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001761 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001762 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1763 # Delete the branches if they exist.
1764 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1765 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1766 result = RunGitWithCode(showref_cmd)
1767 if result[0] == 0:
1768 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769
1770 # We might be in a directory that's present in this branch but not in the
1771 # trunk. Move up to the top of the tree so that git commands that expect a
1772 # valid CWD won't fail after we check out the merge branch.
1773 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1774 if rel_base_path:
1775 os.chdir(rel_base_path)
1776
1777 # Stuff our change into the merge branch.
1778 # We wrap in a try...finally block so if anything goes wrong,
1779 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001780 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001782 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1783 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001785 RunGit(
1786 [
1787 'commit', '--author', options.contributor,
1788 '-m', commit_desc.description,
1789 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001791 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001792 if base_has_submodules:
1793 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1794 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1795 RunGit(['checkout', CHERRY_PICK_BRANCH])
1796 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797 if cmd == 'push':
1798 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001799 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800 retcode, output = RunGitWithCode(
1801 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1802 logging.debug(output)
1803 else:
1804 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001805 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001806 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001807 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808 finally:
1809 # And then swap back to the original branch and clean up.
1810 RunGit(['checkout', '-q', cl.GetBranch()])
1811 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001812 if base_has_submodules:
1813 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814
1815 if cl.GetIssue():
1816 if cmd == 'dcommit' and 'Committed r' in output:
1817 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1818 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001819 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1820 for l in output.splitlines(False))
1821 match = filter(None, match)
1822 if len(match) != 1:
1823 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1824 output)
1825 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001826 else:
1827 return 1
1828 viewvc_url = settings.GetViewVCUrl()
1829 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001830 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001831 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001832 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 print ('Closing issue '
1834 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001835 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001837 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001838 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001839 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001840 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1841 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001842 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001843
1844 if retcode == 0:
1845 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1846 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001847 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 return 0
1850
1851
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001852@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001854 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001856 message = """This doesn't appear to be an SVN repository.
1857If your project has a git mirror with an upstream SVN master, you probably need
1858to run 'git svn init', see your project's git mirror documentation.
1859If your project has a true writeable upstream repository, you probably want
1860to run 'git cl push' instead.
1861Choose wisely, if you get this wrong, your commit might appear to succeed but
1862will instead be silently ignored."""
1863 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001864 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865 return SendUpstream(parser, args, 'dcommit')
1866
1867
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001868@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001869def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001870 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 if settings.GetIsGitSvn():
1872 print('This appears to be an SVN repository.')
1873 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001874 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875 return SendUpstream(parser, args, 'push')
1876
1877
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001878@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001879def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001880 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 parser.add_option('-b', dest='newbranch',
1882 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001883 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001885 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1886 help='Change to the directory DIR immediately, '
1887 'before doing anything else.')
1888 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001889 help='failed patches spew .rej files rather than '
1890 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001891 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1892 help="don't commit after patch applies")
1893 (options, args) = parser.parse_args(args)
1894 if len(args) != 1:
1895 parser.print_help()
1896 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001897 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001899 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001900 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001901
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001902 if options.newbranch:
1903 if options.force:
1904 RunGit(['branch', '-D', options.newbranch],
1905 stderr=subprocess2.PIPE, error_ok=True)
1906 RunGit(['checkout', '-b', options.newbranch,
1907 Changelist().GetUpstreamBranch()])
1908
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001909 return PatchIssue(issue_arg, options.reject, options.nocommit,
1910 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001911
1912
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001913def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001914 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001916 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001917 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001918 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001919 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001921 # Assume it's a URL to the patch. Default to https.
1922 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001923 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001924 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925 DieWithError('Must pass an issue ID or full URL for '
1926 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001927 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001928 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001929 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931 # Switch up to the top-level directory, if necessary, in preparation for
1932 # applying the patch.
1933 top = RunGit(['rev-parse', '--show-cdup']).strip()
1934 if top:
1935 os.chdir(top)
1936
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001937 # Git patches have a/ at the beginning of source paths. We strip that out
1938 # with a sed script rather than the -p flag to patch so we can feed either
1939 # Git or svn-style patches into the same apply command.
1940 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001941 try:
1942 patch_data = subprocess2.check_output(
1943 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1944 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945 DieWithError('Git patch mungling failed.')
1946 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001947 env = os.environ.copy()
1948 # 'cat' is a magical git string that disables pagers on all platforms.
1949 env['GIT_PAGER'] = 'cat'
1950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951 # We use "git apply" to apply the patch instead of "patch" so that we can
1952 # pick up file adds.
1953 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001954 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001955 if directory:
1956 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001957 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001959 elif IsGitVersionAtLeast('1.7.12'):
1960 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001961 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001962 subprocess2.check_call(cmd, env=env,
1963 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001964 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 DieWithError('Failed to apply the patch')
1966
1967 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001968 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001969 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1970 cl = Changelist()
1971 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001972 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001973 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 else:
1975 print "Patch applied to index."
1976 return 0
1977
1978
1979def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001980 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981 # Provide a wrapper for git svn rebase to help avoid accidental
1982 # git svn dcommit.
1983 # It's the only command that doesn't use parser at all since we just defer
1984 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001985 env = os.environ.copy()
1986 # 'cat' is a magical git string that disables pagers on all platforms.
1987 env['GIT_PAGER'] = 'cat'
1988
1989 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990
1991
1992def GetTreeStatus():
1993 """Fetches the tree status and returns either 'open', 'closed',
1994 'unknown' or 'unset'."""
1995 url = settings.GetTreeStatusUrl(error_ok=True)
1996 if url:
1997 status = urllib2.urlopen(url).read().lower()
1998 if status.find('closed') != -1 or status == '0':
1999 return 'closed'
2000 elif status.find('open') != -1 or status == '1':
2001 return 'open'
2002 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002003 return 'unset'
2004
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006def GetTreeStatusReason():
2007 """Fetches the tree status from a json url and returns the message
2008 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002009 url = settings.GetTreeStatusUrl()
2010 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 connection = urllib2.urlopen(json_url)
2012 status = json.loads(connection.read())
2013 connection.close()
2014 return status['message']
2015
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002016
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002017def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002018 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002019 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020 status = GetTreeStatus()
2021 if 'unset' == status:
2022 print 'You must configure your tree status URL by running "git cl config".'
2023 return 2
2024
2025 print "The tree is %s" % status
2026 print
2027 print GetTreeStatusReason()
2028 if status != 'open':
2029 return 1
2030 return 0
2031
2032
maruel@chromium.org15192402012-09-06 12:38:29 +00002033def CMDtry(parser, args):
2034 """Triggers a try job through Rietveld."""
2035 group = optparse.OptionGroup(parser, "Try job options")
2036 group.add_option(
2037 "-b", "--bot", action="append",
2038 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2039 "times to specify multiple builders. ex: "
2040 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2041 "the try server waterfall for the builders name and the tests "
2042 "available. Can also be used to specify gtest_filter, e.g. "
2043 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2044 group.add_option(
2045 "-r", "--revision",
2046 help="Revision to use for the try job; default: the "
2047 "revision will be determined by the try server; see "
2048 "its waterfall for more info")
2049 group.add_option(
2050 "-c", "--clobber", action="store_true", default=False,
2051 help="Force a clobber before building; e.g. don't do an "
2052 "incremental build")
2053 group.add_option(
2054 "--project",
2055 help="Override which project to use. Projects are defined "
2056 "server-side to define what default bot set to use")
2057 group.add_option(
2058 "-t", "--testfilter", action="append", default=[],
2059 help=("Apply a testfilter to all the selected builders. Unless the "
2060 "builders configurations are similar, use multiple "
2061 "--bot <builder>:<test> arguments."))
2062 group.add_option(
2063 "-n", "--name", help="Try job name; default to current branch name")
2064 parser.add_option_group(group)
2065 options, args = parser.parse_args(args)
2066
2067 if args:
2068 parser.error('Unknown arguments: %s' % args)
2069
2070 cl = Changelist()
2071 if not cl.GetIssue():
2072 parser.error('Need to upload first')
2073
2074 if not options.name:
2075 options.name = cl.GetBranch()
2076
2077 # Process --bot and --testfilter.
2078 if not options.bot:
2079 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002080 change = cl.GetChange(
2081 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2082 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002083 options.bot = presubmit_support.DoGetTrySlaves(
2084 change,
2085 change.LocalPaths(),
2086 settings.GetRoot(),
2087 None,
2088 None,
2089 options.verbose,
2090 sys.stdout)
2091 if not options.bot:
2092 parser.error('No default try builder to try, use --bot')
2093
2094 builders_and_tests = {}
2095 for bot in options.bot:
2096 if ':' in bot:
2097 builder, tests = bot.split(':', 1)
2098 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2099 elif ',' in bot:
2100 parser.error('Specify one bot per --bot flag')
2101 else:
2102 builders_and_tests.setdefault(bot, []).append('defaulttests')
2103
2104 if options.testfilter:
2105 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2106 builders_and_tests = dict(
2107 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2108 if t != ['compile'])
2109
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002110 if any('triggered' in b for b in builders_and_tests):
2111 print >> sys.stderr, (
2112 'ERROR You are trying to send a job to a triggered bot. This type of'
2113 ' bot requires an\ninitial job from a parent (usually a builder). '
2114 'Instead send your job to the parent.\n'
2115 'Bot list: %s' % builders_and_tests)
2116 return 1
2117
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002118 patchset = cl.GetMostRecentPatchset()
2119 if patchset and patchset != cl.GetPatchset():
2120 print(
2121 '\nWARNING Mismatch between local config and server. Did a previous '
2122 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2123 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002124
2125 cl.RpcServer().trigger_try_jobs(
2126 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2127 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002128 print('Tried jobs on:')
2129 length = max(len(builder) for builder in builders_and_tests)
2130 for builder in sorted(builders_and_tests):
2131 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002132 return 0
2133
2134
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002135@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002137 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002138 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002139 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002140 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002141
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002142 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002143 if args:
2144 # One arg means set upstream branch.
2145 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2146 cl = Changelist()
2147 print "Upstream branch set to " + cl.GetUpstreamBranch()
2148 else:
2149 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002150 return 0
2151
2152
thestig@chromium.org00858c82013-12-02 23:08:03 +00002153def CMDweb(parser, args):
2154 """Opens the current CL in the web browser."""
2155 _, args = parser.parse_args(args)
2156 if args:
2157 parser.error('Unrecognized args: %s' % ' '.join(args))
2158
2159 issue_url = Changelist().GetIssueURL()
2160 if not issue_url:
2161 print >> sys.stderr, 'ERROR No issue to open'
2162 return 1
2163
2164 webbrowser.open(issue_url)
2165 return 0
2166
2167
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002168def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002169 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002170 _, args = parser.parse_args(args)
2171 if args:
2172 parser.error('Unrecognized args: %s' % ' '.join(args))
2173 cl = Changelist()
2174 cl.SetFlag('commit', '1')
2175 return 0
2176
2177
groby@chromium.org411034a2013-02-26 15:12:01 +00002178def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002179 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002180 _, args = parser.parse_args(args)
2181 if args:
2182 parser.error('Unrecognized args: %s' % ' '.join(args))
2183 cl = Changelist()
2184 # Ensure there actually is an issue to close.
2185 cl.GetDescription()
2186 cl.CloseIssue()
2187 return 0
2188
2189
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002190def CMDdiff(parser, args):
2191 """shows differences between local tree and last upload."""
2192 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002193 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002194 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002195 if not issue:
2196 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002197 TMP_BRANCH = 'git-cl-diff'
2198 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2199
2200 # Create a new branch based on the merge-base
2201 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2202 try:
2203 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002204 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002205 if rtn != 0:
2206 return rtn
2207
2208 # Switch back to starting brand and diff against the temporary
2209 # branch containing the latest rietveld patch.
2210 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2211 finally:
2212 RunGit(['checkout', '-q', branch])
2213 RunGit(['branch', '-D', TMP_BRANCH])
2214
2215 return 0
2216
2217
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002218def CMDowners(parser, args):
2219 """interactively find the owners for reviewing"""
2220 parser.add_option(
2221 '--no-color',
2222 action='store_true',
2223 help='Use this option to disable color output')
2224 options, args = parser.parse_args(args)
2225
2226 author = RunGit(['config', 'user.email']).strip() or None
2227
2228 cl = Changelist()
2229
2230 if args:
2231 if len(args) > 1:
2232 parser.error('Unknown args')
2233 base_branch = args[0]
2234 else:
2235 # Default to diffing against the common ancestor of the upstream branch.
2236 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2237
2238 change = cl.GetChange(base_branch, None)
2239 return owners_finder.OwnersFinder(
2240 [f.LocalPath() for f in
2241 cl.GetChange(base_branch, None).AffectedFiles()],
2242 change.RepositoryRoot(), author,
2243 fopen=file, os_path=os.path, glob=glob.glob,
2244 disable_color=options.no_color).run()
2245
2246
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002247def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002248 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002249 CLANG_EXTS = ['.cc', '.cpp', '.h']
2250 parser.add_option('--full', action='store_true', default=False)
2251 opts, args = parser.parse_args(args)
2252 if args:
2253 parser.error('Unrecognized args: %s' % ' '.join(args))
2254
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002255 # git diff generates paths against the root of the repository. Change
2256 # to that directory so clang-format can find files even within subdirs.
2257 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2258 if rel_base_path:
2259 os.chdir(rel_base_path)
2260
digit@chromium.org29e47272013-05-17 17:01:46 +00002261 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002262 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002263 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002264 # Only list the names of modified files.
2265 diff_cmd.append('--name-only')
2266 else:
2267 # Only generate context-less patches.
2268 diff_cmd.append('-U0')
2269
2270 # Grab the merge-base commit, i.e. the upstream commit of the current
2271 # branch when it was created or the last time it was rebased. This is
2272 # to cover the case where the user may have called "git fetch origin",
2273 # moving the origin branch to a newer commit, but hasn't rebased yet.
2274 upstream_commit = None
2275 cl = Changelist()
2276 upstream_branch = cl.GetUpstreamBranch()
2277 if upstream_branch:
2278 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2279 upstream_commit = upstream_commit.strip()
2280
2281 if not upstream_commit:
2282 DieWithError('Could not find base commit for this branch. '
2283 'Are you in detached state?')
2284
2285 diff_cmd.append(upstream_commit)
2286
2287 # Handle source file filtering.
2288 diff_cmd.append('--')
2289 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2290 diff_output = RunGit(diff_cmd)
2291
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002292 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2293
digit@chromium.org29e47272013-05-17 17:01:46 +00002294 if opts.full:
2295 # diff_output is a list of files to send to clang-format.
2296 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002297 if not files:
2298 print "Nothing to format."
2299 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002300 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2301 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002302 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002303 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002304 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2305 'clang-format-diff.py')
2306 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002307 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002308 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002309
2310 # Newer versions of clang-format-diff.py require an explicit -i flag
2311 # to apply the edits to files, otherwise it just displays a diff.
2312 # Probe the usage string to verify if this is needed.
2313 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2314 if '[-i]' in help_text:
2315 cmd.append('-i')
2316
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002317 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002318
2319 return 0
2320
2321
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002322class OptionParser(optparse.OptionParser):
2323 """Creates the option parse and add --verbose support."""
2324 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002325 optparse.OptionParser.__init__(
2326 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002327 self.add_option(
2328 '-v', '--verbose', action='count', default=0,
2329 help='Use 2 times for more debugging info')
2330
2331 def parse_args(self, args=None, values=None):
2332 options, args = optparse.OptionParser.parse_args(self, args, values)
2333 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2334 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2335 return options, args
2336
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002338def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002339 if sys.hexversion < 0x02060000:
2340 print >> sys.stderr, (
2341 '\nYour python version %s is unsupported, please upgrade.\n' %
2342 sys.version.split(' ', 1)[0])
2343 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002344
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002345 # Reload settings.
2346 global settings
2347 settings = Settings()
2348
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002349 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002350 dispatcher = subcommand.CommandDispatcher(__name__)
2351 try:
2352 return dispatcher.execute(OptionParser(), argv)
2353 except urllib2.HTTPError, e:
2354 if e.code != 500:
2355 raise
2356 DieWithError(
2357 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2358 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002359
2360
2361if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002362 # These affect sys.stdout so do it outside of main() to simplify mocks in
2363 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002364 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002365 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002366 sys.exit(main(sys.argv[1:]))