blob: 90b054222d811ded700f8630c87c0bcbda2c08f0 [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024
25try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000026 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027except ImportError:
28 pass
29
maruel@chromium.org2a74d372011-03-29 19:05:50 +000030
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000031from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032from third_party import upload
33import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000034import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000035import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000037import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000039import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000040import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000042import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043
maruel@chromium.org0633fb42013-08-16 20:06:14 +000044__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000046DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000047POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000049GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000050CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000052# Shortcut since it quickly becomes redundant.
53Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000054
maruel@chromium.orgddd59412011-11-30 14:20:38 +000055# Initialized in main()
56settings = None
57
58
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000060 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 sys.exit(1)
62
63
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000064def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000066 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000067 except subprocess2.CalledProcessError as e:
68 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000069 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 'Command "%s" failed.\n%s' % (
72 ' '.join(args), error_message or e.stdout or ''))
73 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000078 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
80
81def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000082 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000083 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000084 env = os.environ.copy()
85 # 'cat' is a magical git string that disables pagers on all platforms.
86 env['GIT_PAGER'] = 'cat'
87 out, code = subprocess2.communicate(['git'] + args,
88 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000089 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000090 return code, out[0]
91 except ValueError:
92 # When the subprocess fails, it returns None. That triggers a ValueError
93 # when trying to unpack the return value into (out, code).
94 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000095
96
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000097def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000098 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000099 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000100 return (version.startswith(prefix) and
101 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000102
103
maruel@chromium.org90541732011-04-01 17:54:18 +0000104def ask_for_data(prompt):
105 try:
106 return raw_input(prompt)
107 except KeyboardInterrupt:
108 # Hide the exception.
109 sys.exit(1)
110
111
iannucci@chromium.org79540052012-10-19 23:15:26 +0000112def git_set_branch_value(key, value):
113 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000114 if not branch:
115 return
116
117 cmd = ['config']
118 if isinstance(value, int):
119 cmd.append('--int')
120 git_key = 'branch.%s.%s' % (branch, key)
121 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000122
123
124def git_get_branch_default(key, default):
125 branch = Changelist().GetBranch()
126 if branch:
127 git_key = 'branch.%s.%s' % (branch, key)
128 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
129 try:
130 return int(stdout.strip())
131 except ValueError:
132 pass
133 return default
134
135
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000136def add_git_similarity(parser):
137 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000139 help='Sets the percentage that a pair of files need to match in order to'
140 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000141 parser.add_option(
142 '--find-copies', action='store_true',
143 help='Allows git to look for copies.')
144 parser.add_option(
145 '--no-find-copies', action='store_false', dest='find_copies',
146 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000147
148 old_parser_args = parser.parse_args
149 def Parse(args):
150 options, args = old_parser_args(args)
151
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000153 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000154 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 print('Note: Saving similarity of %d%% in git config.'
156 % options.similarity)
157 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000158
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159 options.similarity = max(0, min(options.similarity, 100))
160
161 if options.find_copies is None:
162 options.find_copies = bool(
163 git_get_branch_default('git-find-copies', True))
164 else:
165 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000166
167 print('Using %d%% similarity for rename/copy detection. '
168 'Override with --similarity.' % options.similarity)
169
170 return options, args
171 parser.parse_args = Parse
172
173
ukai@chromium.org259e4682012-10-25 07:36:33 +0000174def is_dirty_git_tree(cmd):
175 # Make sure index is up-to-date before running diff-index.
176 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
177 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
178 if dirty:
179 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
180 print 'Uncommitted files: (git diff-index --name-status HEAD)'
181 print dirty[:4096]
182 if len(dirty) > 4096:
183 print '... (run "git diff-index --name-status HEAD" to see full output).'
184 return True
185 return False
186
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000187
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000188def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
189 """Return the corresponding git ref if |base_url| together with |glob_spec|
190 matches the full |url|.
191
192 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
193 """
194 fetch_suburl, as_ref = glob_spec.split(':')
195 if allow_wildcards:
196 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
197 if glob_match:
198 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
199 # "branches/{472,597,648}/src:refs/remotes/svn/*".
200 branch_re = re.escape(base_url)
201 if glob_match.group(1):
202 branch_re += '/' + re.escape(glob_match.group(1))
203 wildcard = glob_match.group(2)
204 if wildcard == '*':
205 branch_re += '([^/]*)'
206 else:
207 # Escape and replace surrounding braces with parentheses and commas
208 # with pipe symbols.
209 wildcard = re.escape(wildcard)
210 wildcard = re.sub('^\\\\{', '(', wildcard)
211 wildcard = re.sub('\\\\,', '|', wildcard)
212 wildcard = re.sub('\\\\}$', ')', wildcard)
213 branch_re += wildcard
214 if glob_match.group(3):
215 branch_re += re.escape(glob_match.group(3))
216 match = re.match(branch_re, url)
217 if match:
218 return re.sub('\*$', match.group(1), as_ref)
219
220 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
221 if fetch_suburl:
222 full_url = base_url + '/' + fetch_suburl
223 else:
224 full_url = base_url
225 if full_url == url:
226 return as_ref
227 return None
228
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000229
iannucci@chromium.org79540052012-10-19 23:15:26 +0000230def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000231 """Prints statistics about the change to the user."""
232 # --no-ext-diff is broken in some versions of Git, so try to work around
233 # this by overriding the environment (but there is still a problem if the
234 # git config key "diff.external" is used).
235 env = os.environ.copy()
236 if 'GIT_EXTERNAL_DIFF' in env:
237 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000238 # 'cat' is a magical git string that disables pagers on all platforms.
239 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000240
241 if find_copies:
242 similarity_options = ['--find-copies-harder', '-l100000',
243 '-C%s' % similarity]
244 else:
245 similarity_options = ['-M%s' % similarity]
246
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000247 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000248 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000249 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000251
252
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000253class Settings(object):
254 def __init__(self):
255 self.default_server = None
256 self.cc = None
257 self.root = None
258 self.is_git_svn = None
259 self.svn_branch = None
260 self.tree_status_url = None
261 self.viewvc_url = None
262 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000263 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000264 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000265
266 def LazyUpdateIfNeeded(self):
267 """Updates the settings from a codereview.settings file, if available."""
268 if not self.updated:
269 cr_settings_file = FindCodereviewSettingsFile()
270 if cr_settings_file:
271 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000272 self.updated = True
273 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000274 self.updated = True
275
276 def GetDefaultServerUrl(self, error_ok=False):
277 if not self.default_server:
278 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000279 self.default_server = gclient_utils.UpgradeToHttps(
280 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000281 if error_ok:
282 return self.default_server
283 if not self.default_server:
284 error_message = ('Could not find settings file. You must configure '
285 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000286 self.default_server = gclient_utils.UpgradeToHttps(
287 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288 return self.default_server
289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 def GetRoot(self):
291 if not self.root:
292 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
293 return self.root
294
295 def GetIsGitSvn(self):
296 """Return true if this repo looks like it's using git-svn."""
297 if self.is_git_svn is None:
298 # If you have any "svn-remote.*" config keys, we think you're using svn.
299 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000300 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000301 return self.is_git_svn
302
303 def GetSVNBranch(self):
304 if self.svn_branch is None:
305 if not self.GetIsGitSvn():
306 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
307
308 # Try to figure out which remote branch we're based on.
309 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000310 # 1) iterate through our branch history and find the svn URL.
311 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000312
313 # regexp matching the git-svn line that contains the URL.
314 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
315
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000316 env = os.environ.copy()
317 # 'cat' is a magical git string that disables pagers on all platforms.
318 env['GIT_PAGER'] = 'cat'
319
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000320 # We don't want to go through all of history, so read a line from the
321 # pipe at a time.
322 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000323 cmd = ['git', 'log', '-100', '--pretty=medium']
324 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000325 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000326 for line in proc.stdout:
327 match = git_svn_re.match(line)
328 if match:
329 url = match.group(1)
330 proc.stdout.close() # Cut pipe.
331 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000332
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000333 if url:
334 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
335 remotes = RunGit(['config', '--get-regexp',
336 r'^svn-remote\..*\.url']).splitlines()
337 for remote in remotes:
338 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000339 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000340 remote = match.group(1)
341 base_url = match.group(2)
342 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000343 ['config', 'svn-remote.%s.fetch' % remote],
344 error_ok=True).strip()
345 if fetch_spec:
346 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
347 if self.svn_branch:
348 break
349 branch_spec = RunGit(
350 ['config', 'svn-remote.%s.branches' % remote],
351 error_ok=True).strip()
352 if branch_spec:
353 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
354 if self.svn_branch:
355 break
356 tag_spec = RunGit(
357 ['config', 'svn-remote.%s.tags' % remote],
358 error_ok=True).strip()
359 if tag_spec:
360 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
361 if self.svn_branch:
362 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000363
364 if not self.svn_branch:
365 DieWithError('Can\'t guess svn branch -- try specifying it on the '
366 'command line')
367
368 return self.svn_branch
369
370 def GetTreeStatusUrl(self, error_ok=False):
371 if not self.tree_status_url:
372 error_message = ('You must configure your tree status URL by running '
373 '"git cl config".')
374 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
375 error_ok=error_ok,
376 error_message=error_message)
377 return self.tree_status_url
378
379 def GetViewVCUrl(self):
380 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000381 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000382 return self.viewvc_url
383
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000384 def GetDefaultCCList(self):
385 return self._GetConfig('rietveld.cc', error_ok=True)
386
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000387 def GetDefaultPrivateFlag(self):
388 return self._GetConfig('rietveld.private', error_ok=True)
389
ukai@chromium.orge8077812012-02-03 03:41:46 +0000390 def GetIsGerrit(self):
391 """Return true if this repo is assosiated with gerrit code review system."""
392 if self.is_gerrit is None:
393 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
394 return self.is_gerrit
395
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000396 def GetGitEditor(self):
397 """Return the editor specified in the git config, or None if none is."""
398 if self.git_editor is None:
399 self.git_editor = self._GetConfig('core.editor', error_ok=True)
400 return self.git_editor or None
401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 def _GetConfig(self, param, **kwargs):
403 self.LazyUpdateIfNeeded()
404 return RunGit(['config', param], **kwargs).strip()
405
406
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000407def ShortBranchName(branch):
408 """Convert a name like 'refs/heads/foo' to just 'foo'."""
409 return branch.replace('refs/heads/', '')
410
411
412class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000413 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000415 global settings
416 if not settings:
417 # Happens when git_cl.py is used as a utility library.
418 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 settings.GetDefaultServerUrl()
420 self.branchref = branchref
421 if self.branchref:
422 self.branch = ShortBranchName(self.branchref)
423 else:
424 self.branch = None
425 self.rietveld_server = None
426 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000427 self.lookedup_issue = False
428 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000429 self.has_description = False
430 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000431 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000432 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000433 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000434 self.cc = None
435 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000436 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000437 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000438
439 def GetCCList(self):
440 """Return the users cc'd on this CL.
441
442 Return is a string suitable for passing to gcl with the --cc flag.
443 """
444 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000445 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000446 more_cc = ','.join(self.watchers)
447 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
448 return self.cc
449
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000450 def GetCCListWithoutDefault(self):
451 """Return the users cc'd on this CL excluding default ones."""
452 if self.cc is None:
453 self.cc = ','.join(self.watchers)
454 return self.cc
455
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000456 def SetWatchers(self, watchers):
457 """Set the list of email addresses that should be cc'd based on the changed
458 files in this CL.
459 """
460 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000461
462 def GetBranch(self):
463 """Returns the short branch name, e.g. 'master'."""
464 if not self.branch:
465 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
466 self.branch = ShortBranchName(self.branchref)
467 return self.branch
468
469 def GetBranchRef(self):
470 """Returns the full branch name, e.g. 'refs/heads/master'."""
471 self.GetBranch() # Poke the lazy loader.
472 return self.branchref
473
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000474 @staticmethod
475 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000476 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477 e.g. 'origin', 'refs/heads/master'
478 """
479 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000480 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
481 error_ok=True).strip()
482 if upstream_branch:
483 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
484 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000485 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
486 error_ok=True).strip()
487 if upstream_branch:
488 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000489 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000490 # Fall back on trying a git-svn upstream branch.
491 if settings.GetIsGitSvn():
492 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000494 # Else, try to guess the origin remote.
495 remote_branches = RunGit(['branch', '-r']).split()
496 if 'origin/master' in remote_branches:
497 # Fall back on origin/master if it exits.
498 remote = 'origin'
499 upstream_branch = 'refs/heads/master'
500 elif 'origin/trunk' in remote_branches:
501 # Fall back on origin/trunk if it exists. Generally a shared
502 # git-svn clone
503 remote = 'origin'
504 upstream_branch = 'refs/heads/trunk'
505 else:
506 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507Either pass complete "git diff"-style arguments, like
508 git cl upload origin/master
509or verify this branch is set up to track another (via the --track argument to
510"git checkout -b ...").""")
511
512 return remote, upstream_branch
513
514 def GetUpstreamBranch(self):
515 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000517 if remote is not '.':
518 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
519 self.upstream_branch = upstream_branch
520 return self.upstream_branch
521
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000522 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000523 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000524 remote, branch = None, self.GetBranch()
525 seen_branches = set()
526 while branch not in seen_branches:
527 seen_branches.add(branch)
528 remote, branch = self.FetchUpstreamTuple(branch)
529 branch = ShortBranchName(branch)
530 if remote != '.' or branch.startswith('refs/remotes'):
531 break
532 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000533 remotes = RunGit(['remote'], error_ok=True).split()
534 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000535 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000536 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000537 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000538 logging.warning('Could not determine which remote this change is '
539 'associated with, so defaulting to "%s". This may '
540 'not be what you want. You may prevent this message '
541 'by running "git svn info" as documented here: %s',
542 self._remote,
543 GIT_INSTRUCTIONS_URL)
544 else:
545 logging.warn('Could not determine which remote this change is '
546 'associated with. You may prevent this message by '
547 'running "git svn info" as documented here: %s',
548 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000549 branch = 'HEAD'
550 if branch.startswith('refs/remotes'):
551 self._remote = (remote, branch)
552 else:
553 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000554 return self._remote
555
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000556 def GitSanityChecks(self, upstream_git_obj):
557 """Checks git repo status and ensures diff is from local commits."""
558
559 # Verify the commit we're diffing against is in our current branch.
560 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
561 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
562 if upstream_sha != common_ancestor:
563 print >> sys.stderr, (
564 'ERROR: %s is not in the current branch. You may need to rebase '
565 'your tracking branch' % upstream_sha)
566 return False
567
568 # List the commits inside the diff, and verify they are all local.
569 commits_in_diff = RunGit(
570 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
571 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
572 remote_branch = remote_branch.strip()
573 if code != 0:
574 _, remote_branch = self.GetRemoteBranch()
575
576 commits_in_remote = RunGit(
577 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
578
579 common_commits = set(commits_in_diff) & set(commits_in_remote)
580 if common_commits:
581 print >> sys.stderr, (
582 'ERROR: Your diff contains %d commits already in %s.\n'
583 'Run "git log --oneline %s..HEAD" to get a list of commits in '
584 'the diff. If you are using a custom git flow, you can override'
585 ' the reference used for this check with "git config '
586 'gitcl.remotebranch <git-ref>".' % (
587 len(common_commits), remote_branch, upstream_git_obj))
588 return False
589 return True
590
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000591 def GetGitBaseUrlFromConfig(self):
592 """Return the configured base URL from branch.<branchname>.baseurl.
593
594 Returns None if it is not set.
595 """
596 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
597 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599 def GetRemoteUrl(self):
600 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
601
602 Returns None if there is no remote.
603 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000604 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
606
607 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000608 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000609 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000611 self.issue = int(issue) or None if issue else None
612 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 return self.issue
614
615 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000616 if not self.rietveld_server:
617 # If we're on a branch then get the server potentially associated
618 # with that branch.
619 if self.GetIssue():
620 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
621 ['config', self._RietveldServer()], error_ok=True).strip())
622 if not self.rietveld_server:
623 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624 return self.rietveld_server
625
626 def GetIssueURL(self):
627 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000628 if not self.GetIssue():
629 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
631
632 def GetDescription(self, pretty=False):
633 if not self.has_description:
634 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000635 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000636 try:
637 self.description = self.RpcServer().get_description(issue).strip()
638 except urllib2.HTTPError, e:
639 if e.code == 404:
640 DieWithError(
641 ('\nWhile fetching the description for issue %d, received a '
642 '404 (not found)\n'
643 'error. It is likely that you deleted this '
644 'issue on the server. If this is the\n'
645 'case, please run\n\n'
646 ' git cl issue 0\n\n'
647 'to clear the association with the deleted issue. Then run '
648 'this command again.') % issue)
649 else:
650 DieWithError(
651 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 self.has_description = True
653 if pretty:
654 wrapper = textwrap.TextWrapper()
655 wrapper.initial_indent = wrapper.subsequent_indent = ' '
656 return wrapper.fill(self.description)
657 return self.description
658
659 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000660 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000661 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662 patchset = RunGit(['config', self._PatchsetSetting()],
663 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000664 self.patchset = int(patchset) or None if patchset else None
665 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666 return self.patchset
667
668 def SetPatchset(self, patchset):
669 """Set this branch's patchset. If patchset=0, clears the patchset."""
670 if patchset:
671 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673 else:
674 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000675 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000676 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000678 def GetMostRecentPatchset(self):
679 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000680
681 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000682 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000683 '/download/issue%s_%s.diff' % (issue, patchset))
684
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000685 def GetIssueProperties(self):
686 if self._props is None:
687 issue = self.GetIssue()
688 if not issue:
689 self._props = {}
690 else:
691 self._props = self.RpcServer().get_issue_properties(issue, True)
692 return self._props
693
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000694 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000695 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 def SetIssue(self, issue):
698 """Set this branch's issue. If issue=0, clears the issue."""
699 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000700 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000701 RunGit(['config', self._IssueSetting(), str(issue)])
702 if self.rietveld_server:
703 RunGit(['config', self._RietveldServer(), self.rietveld_server])
704 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000705 current_issue = self.GetIssue()
706 if current_issue:
707 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000708 self.issue = None
709 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000711 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000712 if not self.GitSanityChecks(upstream_branch):
713 DieWithError('\nGit sanity check failure')
714
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000715 env = os.environ.copy()
716 # 'cat' is a magical git string that disables pagers on all platforms.
717 env['GIT_PAGER'] = 'cat'
718
719 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000720 if not root:
721 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000722 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000723
724 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000725 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000726 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000727 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000728 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000729 except subprocess2.CalledProcessError:
730 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000731 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000732 'This branch probably doesn\'t exist anymore. To reset the\n'
733 'tracking branch, please run\n'
734 ' git branch --set-upstream %s trunk\n'
735 'replacing trunk with origin/master or the relevant branch') %
736 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000737
maruel@chromium.org52424302012-08-29 15:14:30 +0000738 issue = self.GetIssue()
739 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000740 if issue:
741 description = self.GetDescription()
742 else:
743 # If the change was never uploaded, use the log messages of all commits
744 # up to the branch point, as git cl upload will prefill the description
745 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000746 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000747 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000748 '%s...' % (upstream_branch)],
749 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000750
751 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000752 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000753 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000754 name,
755 description,
756 absroot,
757 files,
758 issue,
759 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000760 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000761
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000762 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000763 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000764
765 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000766 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000767 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000768 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000769 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000770 except presubmit_support.PresubmitFailure, e:
771 DieWithError(
772 ('%s\nMaybe your depot_tools is out of date?\n'
773 'If all fails, contact maruel@') % e)
774
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000775 def UpdateDescription(self, description):
776 self.description = description
777 return self.RpcServer().update_description(
778 self.GetIssue(), self.description)
779
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000781 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000782 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000784 def SetFlag(self, flag, value):
785 """Patchset must match."""
786 if not self.GetPatchset():
787 DieWithError('The patchset needs to match. Send another patchset.')
788 try:
789 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000790 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000791 except urllib2.HTTPError, e:
792 if e.code == 404:
793 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
794 if e.code == 403:
795 DieWithError(
796 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
797 'match?') % (self.GetIssue(), self.GetPatchset()))
798 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000800 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 """Returns an upload.RpcServer() to access this review's rietveld instance.
802 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000803 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000804 self._rpc_server = rietveld.CachingRietveld(
805 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000806 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807
808 def _IssueSetting(self):
809 """Return the git setting that stores this change's issue."""
810 return 'branch.%s.rietveldissue' % self.GetBranch()
811
812 def _PatchsetSetting(self):
813 """Return the git setting that stores this change's most recent patchset."""
814 return 'branch.%s.rietveldpatchset' % self.GetBranch()
815
816 def _RietveldServer(self):
817 """Returns the git setting that stores this change's rietveld server."""
818 return 'branch.%s.rietveldserver' % self.GetBranch()
819
820
821def GetCodereviewSettingsInteractively():
822 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000823 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 server = settings.GetDefaultServerUrl(error_ok=True)
825 prompt = 'Rietveld server (host[:port])'
826 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000827 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 if not server and not newserver:
829 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000830 if newserver:
831 newserver = gclient_utils.UpgradeToHttps(newserver)
832 if newserver != server:
833 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000835 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 prompt = caption
837 if initial:
838 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000839 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840 if new_val == 'x':
841 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000842 elif new_val:
843 if is_url:
844 new_val = gclient_utils.UpgradeToHttps(new_val)
845 if new_val != initial:
846 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000848 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000849 SetProperty(settings.GetDefaultPrivateFlag(),
850 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000852 'tree-status-url', False)
853 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854
855 # TODO: configure a default branch to diff against, rather than this
856 # svn-based hackery.
857
858
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000859class ChangeDescription(object):
860 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000861 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000862 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000863
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000864 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000865 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000866
agable@chromium.org42c20792013-09-12 17:34:49 +0000867 @property # www.logilab.org/ticket/89786
868 def description(self): # pylint: disable=E0202
869 return '\n'.join(self._description_lines)
870
871 def set_description(self, desc):
872 if isinstance(desc, basestring):
873 lines = desc.splitlines()
874 else:
875 lines = [line.rstrip() for line in desc]
876 while lines and not lines[0]:
877 lines.pop(0)
878 while lines and not lines[-1]:
879 lines.pop(-1)
880 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000881
882 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000883 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000884 assert isinstance(reviewers, list), reviewers
885 if not reviewers:
886 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000887 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000888
agable@chromium.org42c20792013-09-12 17:34:49 +0000889 # Get the set of R= and TBR= lines and remove them from the desciption.
890 regexp = re.compile(self.R_LINE)
891 matches = [regexp.match(line) for line in self._description_lines]
892 new_desc = [l for i, l in enumerate(self._description_lines)
893 if not matches[i]]
894 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000895
agable@chromium.org42c20792013-09-12 17:34:49 +0000896 # Construct new unified R= and TBR= lines.
897 r_names = []
898 tbr_names = []
899 for match in matches:
900 if not match:
901 continue
902 people = cleanup_list([match.group(2).strip()])
903 if match.group(1) == 'TBR':
904 tbr_names.extend(people)
905 else:
906 r_names.extend(people)
907 for name in r_names:
908 if name not in reviewers:
909 reviewers.append(name)
910 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
911 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
912
913 # Put the new lines in the description where the old first R= line was.
914 line_loc = next((i for i, match in enumerate(matches) if match), -1)
915 if 0 <= line_loc < len(self._description_lines):
916 if new_tbr_line:
917 self._description_lines.insert(line_loc, new_tbr_line)
918 if new_r_line:
919 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000920 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000921 if new_r_line:
922 self.append_footer(new_r_line)
923 if new_tbr_line:
924 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000925
926 def prompt(self):
927 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000928 self.set_description([
929 '# Enter a description of the change.',
930 '# This will be displayed on the codereview site.',
931 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000932 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000933 '--------------------',
934 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000935
agable@chromium.org42c20792013-09-12 17:34:49 +0000936 regexp = re.compile(self.BUG_LINE)
937 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000938 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000939 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000940 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000941 if not content:
942 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000943 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000944
945 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000946 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
947 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000948 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000949 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000950
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000951 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000952 if self._description_lines:
953 # Add an empty line if either the last line or the new line isn't a tag.
954 last_line = self._description_lines[-1]
955 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
956 not presubmit_support.Change.TAG_LINE_RE.match(line)):
957 self._description_lines.append('')
958 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000959
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000960 def get_reviewers(self):
961 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000962 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
963 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000964 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000965
966
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000967def get_approving_reviewers(props):
968 """Retrieves the reviewers that approved a CL from the issue properties with
969 messages.
970
971 Note that the list may contain reviewers that are not committer, thus are not
972 considered by the CQ.
973 """
974 return sorted(
975 set(
976 message['sender']
977 for message in props['messages']
978 if message['approval'] and message['sender'] in props['reviewers']
979 )
980 )
981
982
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983def FindCodereviewSettingsFile(filename='codereview.settings'):
984 """Finds the given file starting in the cwd and going up.
985
986 Only looks up to the top of the repository unless an
987 'inherit-review-settings-ok' file exists in the root of the repository.
988 """
989 inherit_ok_file = 'inherit-review-settings-ok'
990 cwd = os.getcwd()
991 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
992 if os.path.isfile(os.path.join(root, inherit_ok_file)):
993 root = '/'
994 while True:
995 if filename in os.listdir(cwd):
996 if os.path.isfile(os.path.join(cwd, filename)):
997 return open(os.path.join(cwd, filename))
998 if cwd == root:
999 break
1000 cwd = os.path.dirname(cwd)
1001
1002
1003def LoadCodereviewSettingsFromFile(fileobj):
1004 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001005 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007 def SetProperty(name, setting, unset_error_ok=False):
1008 fullname = 'rietveld.' + name
1009 if setting in keyvals:
1010 RunGit(['config', fullname, keyvals[setting]])
1011 else:
1012 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1013
1014 SetProperty('server', 'CODE_REVIEW_SERVER')
1015 # Only server setting is required. Other settings can be absent.
1016 # In that case, we ignore errors raised during option deletion attempt.
1017 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001018 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1020 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1021
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001022 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001023 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001024
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1026 #should be of the form
1027 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1028 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1029 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1030 keyvals['ORIGIN_URL_CONFIG']])
1031
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001032
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001033def urlretrieve(source, destination):
1034 """urllib is broken for SSL connections via a proxy therefore we
1035 can't use urllib.urlretrieve()."""
1036 with open(destination, 'w') as f:
1037 f.write(urllib2.urlopen(source).read())
1038
1039
ukai@chromium.org712d6102013-11-27 00:52:58 +00001040def hasSheBang(fname):
1041 """Checks fname is a #! script."""
1042 with open(fname) as f:
1043 return f.read(2).startswith('#!')
1044
1045
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001046def DownloadHooks(force):
1047 """downloads hooks
1048
1049 Args:
1050 force: True to update hooks. False to install hooks if not present.
1051 """
1052 if not settings.GetIsGerrit():
1053 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001054 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001055 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1056 if not os.access(dst, os.X_OK):
1057 if os.path.exists(dst):
1058 if not force:
1059 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001060 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001061 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001062 if not hasSheBang(dst):
1063 DieWithError('Not a script: %s\n'
1064 'You need to download from\n%s\n'
1065 'into .git/hooks/commit-msg and '
1066 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001067 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1068 except Exception:
1069 if os.path.exists(dst):
1070 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001071 DieWithError('\nFailed to download hooks.\n'
1072 'You need to download from\n%s\n'
1073 'into .git/hooks/commit-msg and '
1074 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001075
1076
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001077@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001079 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001081 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082 if len(args) == 0:
1083 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001084 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 return 0
1086
1087 url = args[0]
1088 if not url.endswith('codereview.settings'):
1089 url = os.path.join(url, 'codereview.settings')
1090
1091 # Load code review settings and download hooks (if available).
1092 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001093 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 return 0
1095
1096
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001097def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001098 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001099 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1100 branch = ShortBranchName(branchref)
1101 _, args = parser.parse_args(args)
1102 if not args:
1103 print("Current base-url:")
1104 return RunGit(['config', 'branch.%s.base-url' % branch],
1105 error_ok=False).strip()
1106 else:
1107 print("Setting base-url to %s" % args[0])
1108 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1109 error_ok=False).strip()
1110
1111
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001113 """Show status of changelists.
1114
1115 Colors are used to tell the state of the CL unless --fast is used:
1116 - Green LGTM'ed
1117 - Blue waiting for review
1118 - Yellow waiting for you to reply to review
1119 - Red not sent for review or broken
1120 - Cyan was committed, branch can be deleted
1121
1122 Also see 'git cl comments'.
1123 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124 parser.add_option('--field',
1125 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001126 parser.add_option('-f', '--fast', action='store_true',
1127 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001129 if args:
1130 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001133 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 if options.field.startswith('desc'):
1135 print cl.GetDescription()
1136 elif options.field == 'id':
1137 issueid = cl.GetIssue()
1138 if issueid:
1139 print issueid
1140 elif options.field == 'patch':
1141 patchset = cl.GetPatchset()
1142 if patchset:
1143 print patchset
1144 elif options.field == 'url':
1145 url = cl.GetIssueURL()
1146 if url:
1147 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001148 return 0
1149
1150 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1151 if not branches:
1152 print('No local branch found.')
1153 return 0
1154
1155 changes = (Changelist(branchref=b) for b in branches.splitlines())
1156 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1157 alignment = max(5, max(len(b) for b in branches))
1158 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001159 # Adhoc thread pool to request data concurrently.
1160 output = Queue.Queue()
1161
1162 # Silence upload.py otherwise it becomes unweldly.
1163 upload.verbosity = 0
1164
1165 if not options.fast:
1166 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001167 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001168 c = Changelist(branchref=b)
1169 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001170 props = {}
1171 r = None
1172 if i:
1173 try:
1174 props = c.GetIssueProperties()
1175 r = c.GetApprovingReviewers() if i else None
1176 except urllib2.HTTPError:
1177 # The issue probably doesn't exist anymore.
1178 i += ' (broken)'
1179
1180 msgs = props.get('messages') or []
1181
1182 if not i:
1183 color = Fore.WHITE
1184 elif props.get('closed'):
1185 # Issue is closed.
1186 color = Fore.CYAN
1187 elif r:
1188 # Was LGTM'ed.
1189 color = Fore.GREEN
1190 elif not msgs:
1191 # No message was sent.
1192 color = Fore.RED
1193 elif msgs[-1]['sender'] != props.get('owner_email'):
1194 color = Fore.YELLOW
1195 else:
1196 color = Fore.BLUE
1197 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001198
1199 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1200 for t in threads:
1201 t.daemon = True
1202 t.start()
1203 else:
1204 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1205 for b in branches:
1206 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001207 url = c.GetIssueURL()
1208 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001209
1210 tmp = {}
1211 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001212 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001213 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001214 b, i, color = output.get()
1215 tmp[b] = (i, color)
1216 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001217 reset = Fore.RESET
1218 if not sys.stdout.isatty():
1219 color = ''
1220 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001221 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001222 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001223
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001224 cl = Changelist()
1225 print
1226 print 'Current branch:',
1227 if not cl.GetIssue():
1228 print 'no issue assigned.'
1229 return 0
1230 print cl.GetBranch()
1231 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1232 print 'Issue description:'
1233 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 return 0
1235
1236
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001237def colorize_CMDstatus_doc():
1238 """To be called once in main() to add colors to git cl status help."""
1239 colors = [i for i in dir(Fore) if i[0].isupper()]
1240
1241 def colorize_line(line):
1242 for color in colors:
1243 if color in line.upper():
1244 # Extract whitespaces first and the leading '-'.
1245 indent = len(line) - len(line.lstrip(' ')) + 1
1246 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1247 return line
1248
1249 lines = CMDstatus.__doc__.splitlines()
1250 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1251
1252
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001253@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001255 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256
1257 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001258 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001259 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260
1261 cl = Changelist()
1262 if len(args) > 0:
1263 try:
1264 issue = int(args[0])
1265 except ValueError:
1266 DieWithError('Pass a number to set the issue or none to list it.\n'
1267 'Maybe you want to run git cl status?')
1268 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001269 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 return 0
1271
1272
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001273def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001274 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001275 (_, args) = parser.parse_args(args)
1276 if args:
1277 parser.error('Unsupported argument: %s' % args)
1278
1279 cl = Changelist()
1280 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001281 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001282 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001283 if message['disapproval']:
1284 color = Fore.RED
1285 elif message['approval']:
1286 color = Fore.GREEN
1287 elif message['sender'] == data['owner_email']:
1288 color = Fore.MAGENTA
1289 else:
1290 color = Fore.BLUE
1291 print '\n%s%s %s%s' % (
1292 color, message['date'].split('.', 1)[0], message['sender'],
1293 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001294 if message['text'].strip():
1295 print '\n'.join(' ' + l for l in message['text'].splitlines())
1296 return 0
1297
1298
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001299def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001300 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001301 cl = Changelist()
1302 if not cl.GetIssue():
1303 DieWithError('This branch has no associated changelist.')
1304 description = ChangeDescription(cl.GetDescription())
1305 description.prompt()
1306 cl.UpdateDescription(description.description)
1307 return 0
1308
1309
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310def CreateDescriptionFromLog(args):
1311 """Pulls out the commit log to use as a base for the CL description."""
1312 log_args = []
1313 if len(args) == 1 and not args[0].endswith('.'):
1314 log_args = [args[0] + '..']
1315 elif len(args) == 1 and args[0].endswith('...'):
1316 log_args = [args[0][:-1]]
1317 elif len(args) == 2:
1318 log_args = [args[0] + '..' + args[1]]
1319 else:
1320 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001321 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001325 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001326 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001328 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001329 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 (options, args) = parser.parse_args(args)
1331
ukai@chromium.org259e4682012-10-25 07:36:33 +00001332 if not options.force and is_dirty_git_tree('presubmit'):
1333 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 return 1
1335
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001336 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 if args:
1338 base_branch = args[0]
1339 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 # Default to diffing against the common ancestor of the upstream branch.
1341 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001343 cl.RunHook(
1344 committing=not options.upload,
1345 may_prompt=False,
1346 verbose=options.verbose,
1347 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001348 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
1350
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001351def AddChangeIdToCommitMessage(options, args):
1352 """Re-commits using the current message, assumes the commit hook is in
1353 place.
1354 """
1355 log_desc = options.message or CreateDescriptionFromLog(args)
1356 git_command = ['commit', '--amend', '-m', log_desc]
1357 RunGit(git_command)
1358 new_log_desc = CreateDescriptionFromLog(args)
1359 if CHANGE_ID in new_log_desc:
1360 print 'git-cl: Added Change-Id to commit message.'
1361 else:
1362 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1363
1364
ukai@chromium.orge8077812012-02-03 03:41:46 +00001365def GerritUpload(options, args, cl):
1366 """upload the current branch to gerrit."""
1367 # We assume the remote called "origin" is the one we want.
1368 # It is probably not worthwhile to support different workflows.
1369 remote = 'origin'
1370 branch = 'master'
1371 if options.target_branch:
1372 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001374 change_desc = ChangeDescription(
1375 options.message or CreateDescriptionFromLog(args))
1376 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001377 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001379 if CHANGE_ID not in change_desc.description:
1380 AddChangeIdToCommitMessage(options, args)
1381 if options.reviewers:
1382 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383
ukai@chromium.orge8077812012-02-03 03:41:46 +00001384 receive_options = []
1385 cc = cl.GetCCList().split(',')
1386 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001387 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001388 cc = filter(None, cc)
1389 if cc:
1390 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001391 if change_desc.get_reviewers():
1392 receive_options.extend(
1393 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394
ukai@chromium.orge8077812012-02-03 03:41:46 +00001395 git_command = ['push']
1396 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001397 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001398 ' '.join(receive_options))
1399 git_command += [remote, 'HEAD:refs/for/' + branch]
1400 RunGit(git_command)
1401 # TODO(ukai): parse Change-Id: and set issue number?
1402 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404
ukai@chromium.orge8077812012-02-03 03:41:46 +00001405def RietveldUpload(options, args, cl):
1406 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1408 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 if options.emulate_svn_auto_props:
1410 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
1412 change_desc = None
1413
1414 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001415 if options.title:
1416 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001417 if options.message:
1418 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001419 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 print ("This branch is associated with issue %s. "
1421 "Adding patch to that issue." % cl.GetIssue())
1422 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001423 if options.title:
1424 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001425 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001426 change_desc = ChangeDescription(message)
1427 if options.reviewers:
1428 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001429 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001430 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001431
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001432 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 print "Description is empty; aborting."
1434 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001435
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001436 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001437 if change_desc.get_reviewers():
1438 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001439 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001440 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001441 DieWithError("Must specify reviewers to send email.")
1442 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001443
1444 # We check this before applying rietveld.private assuming that in
1445 # rietveld.cc only addresses which we can send private CLs to are listed
1446 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1447 # --private is specified explicitly on the command line.
1448 if options.private:
1449 logging.warn('rietveld.cc is ignored since private flag is specified. '
1450 'You need to review and add them manually if necessary.')
1451 cc = cl.GetCCListWithoutDefault()
1452 else:
1453 cc = cl.GetCCList()
1454 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001455 if cc:
1456 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001458 if options.private or settings.GetDefaultPrivateFlag() == "True":
1459 upload_args.append('--private')
1460
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001461 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001462 if not options.find_copies:
1463 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001464
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 # Include the upstream repo's URL in the change -- this is useful for
1466 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001467 remote_url = cl.GetGitBaseUrlFromConfig()
1468 if not remote_url:
1469 if settings.GetIsGitSvn():
1470 # URL is dependent on the current directory.
1471 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1472 if data:
1473 keys = dict(line.split(': ', 1) for line in data.splitlines()
1474 if ': ' in line)
1475 remote_url = keys.get('URL', None)
1476 else:
1477 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1478 remote_url = (cl.GetRemoteUrl() + '@'
1479 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480 if remote_url:
1481 upload_args.extend(['--base_url', remote_url])
1482
1483 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001484 upload_args = ['upload'] + upload_args + args
1485 logging.info('upload.RealMain(%s)', upload_args)
1486 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001487 issue = int(issue)
1488 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001489 except KeyboardInterrupt:
1490 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491 except:
1492 # If we got an exception after the user typed a description for their
1493 # change, back up the description before re-raising.
1494 if change_desc:
1495 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1496 print '\nGot exception while uploading -- saving description to %s\n' \
1497 % backup_path
1498 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001499 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500 backup_file.close()
1501 raise
1502
1503 if not cl.GetIssue():
1504 cl.SetIssue(issue)
1505 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001506
1507 if options.use_commit_queue:
1508 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001509 return 0
1510
1511
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001512def cleanup_list(l):
1513 """Fixes a list so that comma separated items are put as individual items.
1514
1515 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1516 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1517 """
1518 items = sum((i.split(',') for i in l), [])
1519 stripped_items = (i.strip() for i in items)
1520 return sorted(filter(None, stripped_items))
1521
1522
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001523@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001524def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001525 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001526 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1527 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001528 parser.add_option('--bypass-watchlists', action='store_true',
1529 dest='bypass_watchlists',
1530 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001531 parser.add_option('-f', action='store_true', dest='force',
1532 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001533 parser.add_option('-m', dest='message', help='message for patchset')
1534 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001535 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001536 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001537 help='reviewer email addresses')
1538 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001539 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001540 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001541 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001542 help='send email to reviewer immediately')
1543 parser.add_option("--emulate_svn_auto_props", action="store_true",
1544 dest="emulate_svn_auto_props",
1545 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001546 parser.add_option('-c', '--use-commit-queue', action='store_true',
1547 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001548 parser.add_option('--private', action='store_true',
1549 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001550 parser.add_option('--target_branch',
1551 help='When uploading to gerrit, remote branch to '
1552 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001553 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001554 (options, args) = parser.parse_args(args)
1555
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001556 if options.target_branch and not settings.GetIsGerrit():
1557 parser.error('Use --target_branch for non gerrit repository.')
1558
ukai@chromium.org259e4682012-10-25 07:36:33 +00001559 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001560 return 1
1561
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001562 options.reviewers = cleanup_list(options.reviewers)
1563 options.cc = cleanup_list(options.cc)
1564
ukai@chromium.orge8077812012-02-03 03:41:46 +00001565 cl = Changelist()
1566 if args:
1567 # TODO(ukai): is it ok for gerrit case?
1568 base_branch = args[0]
1569 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001570 # Default to diffing against common ancestor of upstream branch
1571 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001572 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001574 # Apply watchlists on upload.
1575 change = cl.GetChange(base_branch, None)
1576 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1577 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001578 if not options.bypass_watchlists:
1579 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001580
ukai@chromium.orge8077812012-02-03 03:41:46 +00001581 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001582 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001583 may_prompt=not options.force,
1584 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001585 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001586 if not hook_results.should_continue():
1587 return 1
1588 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001589 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001590
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001591 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001592 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001593 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001594 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001595 print ('The last upload made from this repository was patchset #%d but '
1596 'the most recent patchset on the server is #%d.'
1597 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001598 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1599 'from another machine or branch the patch you\'re uploading now '
1600 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001601 ask_for_data('About to upload; enter to confirm.')
1602
iannucci@chromium.org79540052012-10-19 23:15:26 +00001603 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001604 if settings.GetIsGerrit():
1605 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001606 ret = RietveldUpload(options, args, cl)
1607 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001608 git_set_branch_value('last-upload-hash',
1609 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001610
1611 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001612
1613
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001614def IsSubmoduleMergeCommit(ref):
1615 # When submodules are added to the repo, we expect there to be a single
1616 # non-git-svn merge commit at remote HEAD with a signature comment.
1617 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001618 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001619 return RunGit(cmd) != ''
1620
1621
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622def SendUpstream(parser, args, cmd):
1623 """Common code for CmdPush and CmdDCommit
1624
1625 Squashed commit into a single.
1626 Updates changelog with metadata (e.g. pointer to review).
1627 Pushes/dcommits the code upstream.
1628 Updates review and closes.
1629 """
1630 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1631 help='bypass upload presubmit hook')
1632 parser.add_option('-m', dest='message',
1633 help="override review description")
1634 parser.add_option('-f', action='store_true', dest='force',
1635 help="force yes to questions (don't prompt)")
1636 parser.add_option('-c', dest='contributor',
1637 help="external contributor for patch (appended to " +
1638 "description and used as author for git). Should be " +
1639 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001640 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641 (options, args) = parser.parse_args(args)
1642 cl = Changelist()
1643
1644 if not args or cmd == 'push':
1645 # Default to merging against our best guess of the upstream branch.
1646 args = [cl.GetUpstreamBranch()]
1647
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001648 if options.contributor:
1649 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1650 print "Please provide contibutor as 'First Last <email@example.com>'"
1651 return 1
1652
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001654 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655
ukai@chromium.org259e4682012-10-25 07:36:33 +00001656 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001657 return 1
1658
1659 # This rev-list syntax means "show all commits not in my branch that
1660 # are in base_branch".
1661 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1662 base_branch]).splitlines()
1663 if upstream_commits:
1664 print ('Base branch "%s" has %d commits '
1665 'not in this branch.' % (base_branch, len(upstream_commits)))
1666 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1667 return 1
1668
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001669 # This is the revision `svn dcommit` will commit on top of.
1670 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1671 '--pretty=format:%H'])
1672
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001674 # If the base_head is a submodule merge commit, the first parent of the
1675 # base_head should be a git-svn commit, which is what we're interested in.
1676 base_svn_head = base_branch
1677 if base_has_submodules:
1678 base_svn_head += '^1'
1679
1680 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681 if extra_commits:
1682 print ('This branch has %d additional commits not upstreamed yet.'
1683 % len(extra_commits.splitlines()))
1684 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1685 'before attempting to %s.' % (base_branch, cmd))
1686 return 1
1687
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001688 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001689 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001690 author = None
1691 if options.contributor:
1692 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001693 hook_results = cl.RunHook(
1694 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001695 may_prompt=not options.force,
1696 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001697 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001698 if not hook_results.should_continue():
1699 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700
1701 if cmd == 'dcommit':
1702 # Check the tree status if the tree status URL is set.
1703 status = GetTreeStatus()
1704 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001705 print('The tree is closed. Please wait for it to reopen. Use '
1706 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707 return 1
1708 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001709 print('Unable to determine tree status. Please verify manually and '
1710 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001711 else:
1712 breakpad.SendStack(
1713 'GitClHooksBypassedCommit',
1714 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001715 (cl.GetRietveldServer(), cl.GetIssue()),
1716 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001718 change_desc = ChangeDescription(options.message)
1719 if not change_desc.description and cl.GetIssue():
1720 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001721
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001722 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001723 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001724 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001725 else:
1726 print 'No description set.'
1727 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1728 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001729
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001730 # Keep a separate copy for the commit message, because the commit message
1731 # contains the link to the Rietveld issue, while the Rietveld message contains
1732 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001733 # Keep a separate copy for the commit message.
1734 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001735 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001736
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001737 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001738 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001739 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001741 commit_desc.append_footer('Patch from %s.' % options.contributor)
1742
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001743 print('Description:')
1744 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745
1746 branches = [base_branch, cl.GetBranchRef()]
1747 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001748 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001749 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001751 # We want to squash all this branch's commits into one commit with the proper
1752 # description. We do this by doing a "reset --soft" to the base branch (which
1753 # keeps the working copy the same), then dcommitting that. If origin/master
1754 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1755 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001757 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1758 # Delete the branches if they exist.
1759 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1760 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1761 result = RunGitWithCode(showref_cmd)
1762 if result[0] == 0:
1763 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764
1765 # We might be in a directory that's present in this branch but not in the
1766 # trunk. Move up to the top of the tree so that git commands that expect a
1767 # valid CWD won't fail after we check out the merge branch.
1768 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1769 if rel_base_path:
1770 os.chdir(rel_base_path)
1771
1772 # Stuff our change into the merge branch.
1773 # We wrap in a try...finally block so if anything goes wrong,
1774 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001775 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001776 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001777 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1778 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001780 RunGit(
1781 [
1782 'commit', '--author', options.contributor,
1783 '-m', commit_desc.description,
1784 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001785 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001786 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001787 if base_has_submodules:
1788 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1789 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1790 RunGit(['checkout', CHERRY_PICK_BRANCH])
1791 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001792 if cmd == 'push':
1793 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001794 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 retcode, output = RunGitWithCode(
1796 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1797 logging.debug(output)
1798 else:
1799 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001800 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001801 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001802 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803 finally:
1804 # And then swap back to the original branch and clean up.
1805 RunGit(['checkout', '-q', cl.GetBranch()])
1806 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001807 if base_has_submodules:
1808 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001809
1810 if cl.GetIssue():
1811 if cmd == 'dcommit' and 'Committed r' in output:
1812 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1813 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001814 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1815 for l in output.splitlines(False))
1816 match = filter(None, match)
1817 if len(match) != 1:
1818 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1819 output)
1820 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001821 else:
1822 return 1
1823 viewvc_url = settings.GetViewVCUrl()
1824 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001825 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001826 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001827 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828 print ('Closing issue '
1829 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001830 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001832 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001833 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001834 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001835 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1836 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001837 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001838
1839 if retcode == 0:
1840 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1841 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001842 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001843
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844 return 0
1845
1846
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001847@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001848def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001849 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001850 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001851 message = """This doesn't appear to be an SVN repository.
1852If your project has a git mirror with an upstream SVN master, you probably need
1853to run 'git svn init', see your project's git mirror documentation.
1854If your project has a true writeable upstream repository, you probably want
1855to run 'git cl push' instead.
1856Choose wisely, if you get this wrong, your commit might appear to succeed but
1857will instead be silently ignored."""
1858 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001859 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 return SendUpstream(parser, args, 'dcommit')
1861
1862
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001863@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001865 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001866 if settings.GetIsGitSvn():
1867 print('This appears to be an SVN repository.')
1868 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001869 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001870 return SendUpstream(parser, args, 'push')
1871
1872
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001873@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001875 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876 parser.add_option('-b', dest='newbranch',
1877 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001878 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001879 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001880 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1881 help='Change to the directory DIR immediately, '
1882 'before doing anything else.')
1883 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001884 help='failed patches spew .rej files rather than '
1885 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1887 help="don't commit after patch applies")
1888 (options, args) = parser.parse_args(args)
1889 if len(args) != 1:
1890 parser.print_help()
1891 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001892 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001894 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001895 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001896
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001897 if options.newbranch:
1898 if options.force:
1899 RunGit(['branch', '-D', options.newbranch],
1900 stderr=subprocess2.PIPE, error_ok=True)
1901 RunGit(['checkout', '-b', options.newbranch,
1902 Changelist().GetUpstreamBranch()])
1903
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001904 return PatchIssue(issue_arg, options.reject, options.nocommit,
1905 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001906
1907
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001908def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001909 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001911 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001912 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001913 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001914 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001916 # Assume it's a URL to the patch. Default to https.
1917 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001918 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001919 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920 DieWithError('Must pass an issue ID or full URL for '
1921 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001922 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001923 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001924 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926 # Switch up to the top-level directory, if necessary, in preparation for
1927 # applying the patch.
1928 top = RunGit(['rev-parse', '--show-cdup']).strip()
1929 if top:
1930 os.chdir(top)
1931
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932 # Git patches have a/ at the beginning of source paths. We strip that out
1933 # with a sed script rather than the -p flag to patch so we can feed either
1934 # Git or svn-style patches into the same apply command.
1935 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001936 try:
1937 patch_data = subprocess2.check_output(
1938 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1939 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940 DieWithError('Git patch mungling failed.')
1941 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001942 env = os.environ.copy()
1943 # 'cat' is a magical git string that disables pagers on all platforms.
1944 env['GIT_PAGER'] = 'cat'
1945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946 # We use "git apply" to apply the patch instead of "patch" so that we can
1947 # pick up file adds.
1948 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001949 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001950 if directory:
1951 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001952 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001953 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001954 elif IsGitVersionAtLeast('1.7.12'):
1955 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001956 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001957 subprocess2.check_call(cmd, env=env,
1958 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001959 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001960 DieWithError('Failed to apply the patch')
1961
1962 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001963 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1965 cl = Changelist()
1966 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001967 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001968 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001969 else:
1970 print "Patch applied to index."
1971 return 0
1972
1973
1974def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001975 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 # Provide a wrapper for git svn rebase to help avoid accidental
1977 # git svn dcommit.
1978 # It's the only command that doesn't use parser at all since we just defer
1979 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001980 env = os.environ.copy()
1981 # 'cat' is a magical git string that disables pagers on all platforms.
1982 env['GIT_PAGER'] = 'cat'
1983
1984 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985
1986
1987def GetTreeStatus():
1988 """Fetches the tree status and returns either 'open', 'closed',
1989 'unknown' or 'unset'."""
1990 url = settings.GetTreeStatusUrl(error_ok=True)
1991 if url:
1992 status = urllib2.urlopen(url).read().lower()
1993 if status.find('closed') != -1 or status == '0':
1994 return 'closed'
1995 elif status.find('open') != -1 or status == '1':
1996 return 'open'
1997 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998 return 'unset'
1999
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001def GetTreeStatusReason():
2002 """Fetches the tree status from a json url and returns the message
2003 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002004 url = settings.GetTreeStatusUrl()
2005 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 connection = urllib2.urlopen(json_url)
2007 status = json.loads(connection.read())
2008 connection.close()
2009 return status['message']
2010
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002011
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002012def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002013 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002014 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015 status = GetTreeStatus()
2016 if 'unset' == status:
2017 print 'You must configure your tree status URL by running "git cl config".'
2018 return 2
2019
2020 print "The tree is %s" % status
2021 print
2022 print GetTreeStatusReason()
2023 if status != 'open':
2024 return 1
2025 return 0
2026
2027
maruel@chromium.org15192402012-09-06 12:38:29 +00002028def CMDtry(parser, args):
2029 """Triggers a try job through Rietveld."""
2030 group = optparse.OptionGroup(parser, "Try job options")
2031 group.add_option(
2032 "-b", "--bot", action="append",
2033 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2034 "times to specify multiple builders. ex: "
2035 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2036 "the try server waterfall for the builders name and the tests "
2037 "available. Can also be used to specify gtest_filter, e.g. "
2038 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2039 group.add_option(
2040 "-r", "--revision",
2041 help="Revision to use for the try job; default: the "
2042 "revision will be determined by the try server; see "
2043 "its waterfall for more info")
2044 group.add_option(
2045 "-c", "--clobber", action="store_true", default=False,
2046 help="Force a clobber before building; e.g. don't do an "
2047 "incremental build")
2048 group.add_option(
2049 "--project",
2050 help="Override which project to use. Projects are defined "
2051 "server-side to define what default bot set to use")
2052 group.add_option(
2053 "-t", "--testfilter", action="append", default=[],
2054 help=("Apply a testfilter to all the selected builders. Unless the "
2055 "builders configurations are similar, use multiple "
2056 "--bot <builder>:<test> arguments."))
2057 group.add_option(
2058 "-n", "--name", help="Try job name; default to current branch name")
2059 parser.add_option_group(group)
2060 options, args = parser.parse_args(args)
2061
2062 if args:
2063 parser.error('Unknown arguments: %s' % args)
2064
2065 cl = Changelist()
2066 if not cl.GetIssue():
2067 parser.error('Need to upload first')
2068
2069 if not options.name:
2070 options.name = cl.GetBranch()
2071
2072 # Process --bot and --testfilter.
2073 if not options.bot:
2074 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002075 change = cl.GetChange(
2076 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2077 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002078 options.bot = presubmit_support.DoGetTrySlaves(
2079 change,
2080 change.LocalPaths(),
2081 settings.GetRoot(),
2082 None,
2083 None,
2084 options.verbose,
2085 sys.stdout)
2086 if not options.bot:
2087 parser.error('No default try builder to try, use --bot')
2088
2089 builders_and_tests = {}
2090 for bot in options.bot:
2091 if ':' in bot:
2092 builder, tests = bot.split(':', 1)
2093 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2094 elif ',' in bot:
2095 parser.error('Specify one bot per --bot flag')
2096 else:
2097 builders_and_tests.setdefault(bot, []).append('defaulttests')
2098
2099 if options.testfilter:
2100 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2101 builders_and_tests = dict(
2102 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2103 if t != ['compile'])
2104
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002105 if any('triggered' in b for b in builders_and_tests):
2106 print >> sys.stderr, (
2107 'ERROR You are trying to send a job to a triggered bot. This type of'
2108 ' bot requires an\ninitial job from a parent (usually a builder). '
2109 'Instead send your job to the parent.\n'
2110 'Bot list: %s' % builders_and_tests)
2111 return 1
2112
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002113 patchset = cl.GetMostRecentPatchset()
2114 if patchset and patchset != cl.GetPatchset():
2115 print(
2116 '\nWARNING Mismatch between local config and server. Did a previous '
2117 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2118 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002119
2120 cl.RpcServer().trigger_try_jobs(
2121 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2122 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002123 print('Tried jobs on:')
2124 length = max(len(builder) for builder in builders_and_tests)
2125 for builder in sorted(builders_and_tests):
2126 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002127 return 0
2128
2129
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002130@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002131def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002132 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002133 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002134 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002135 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002136 return 0
2137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002139 if args:
2140 # One arg means set upstream branch.
2141 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2142 cl = Changelist()
2143 print "Upstream branch set to " + cl.GetUpstreamBranch()
2144 else:
2145 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002146 return 0
2147
2148
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002149def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002150 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002151 _, args = parser.parse_args(args)
2152 if args:
2153 parser.error('Unrecognized args: %s' % ' '.join(args))
2154 cl = Changelist()
2155 cl.SetFlag('commit', '1')
2156 return 0
2157
2158
groby@chromium.org411034a2013-02-26 15:12:01 +00002159def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002160 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002161 _, args = parser.parse_args(args)
2162 if args:
2163 parser.error('Unrecognized args: %s' % ' '.join(args))
2164 cl = Changelist()
2165 # Ensure there actually is an issue to close.
2166 cl.GetDescription()
2167 cl.CloseIssue()
2168 return 0
2169
2170
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002171def CMDdiff(parser, args):
2172 """shows differences between local tree and last upload."""
2173 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002174 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002175 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002176 if not issue:
2177 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002178 TMP_BRANCH = 'git-cl-diff'
2179 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2180
2181 # Create a new branch based on the merge-base
2182 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2183 try:
2184 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002185 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002186 if rtn != 0:
2187 return rtn
2188
2189 # Switch back to starting brand and diff against the temporary
2190 # branch containing the latest rietveld patch.
2191 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2192 finally:
2193 RunGit(['checkout', '-q', branch])
2194 RunGit(['branch', '-D', TMP_BRANCH])
2195
2196 return 0
2197
2198
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002199def CMDowners(parser, args):
2200 """interactively find the owners for reviewing"""
2201 parser.add_option(
2202 '--no-color',
2203 action='store_true',
2204 help='Use this option to disable color output')
2205 options, args = parser.parse_args(args)
2206
2207 author = RunGit(['config', 'user.email']).strip() or None
2208
2209 cl = Changelist()
2210
2211 if args:
2212 if len(args) > 1:
2213 parser.error('Unknown args')
2214 base_branch = args[0]
2215 else:
2216 # Default to diffing against the common ancestor of the upstream branch.
2217 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2218
2219 change = cl.GetChange(base_branch, None)
2220 return owners_finder.OwnersFinder(
2221 [f.LocalPath() for f in
2222 cl.GetChange(base_branch, None).AffectedFiles()],
2223 change.RepositoryRoot(), author,
2224 fopen=file, os_path=os.path, glob=glob.glob,
2225 disable_color=options.no_color).run()
2226
2227
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002228def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002229 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002230 CLANG_EXTS = ['.cc', '.cpp', '.h']
2231 parser.add_option('--full', action='store_true', default=False)
2232 opts, args = parser.parse_args(args)
2233 if args:
2234 parser.error('Unrecognized args: %s' % ' '.join(args))
2235
digit@chromium.org29e47272013-05-17 17:01:46 +00002236 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002237 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002238 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002239 # Only list the names of modified files.
2240 diff_cmd.append('--name-only')
2241 else:
2242 # Only generate context-less patches.
2243 diff_cmd.append('-U0')
2244
2245 # Grab the merge-base commit, i.e. the upstream commit of the current
2246 # branch when it was created or the last time it was rebased. This is
2247 # to cover the case where the user may have called "git fetch origin",
2248 # moving the origin branch to a newer commit, but hasn't rebased yet.
2249 upstream_commit = None
2250 cl = Changelist()
2251 upstream_branch = cl.GetUpstreamBranch()
2252 if upstream_branch:
2253 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2254 upstream_commit = upstream_commit.strip()
2255
2256 if not upstream_commit:
2257 DieWithError('Could not find base commit for this branch. '
2258 'Are you in detached state?')
2259
2260 diff_cmd.append(upstream_commit)
2261
2262 # Handle source file filtering.
2263 diff_cmd.append('--')
2264 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2265 diff_output = RunGit(diff_cmd)
2266
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002267 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2268
digit@chromium.org29e47272013-05-17 17:01:46 +00002269 if opts.full:
2270 # diff_output is a list of files to send to clang-format.
2271 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002272 if not files:
2273 print "Nothing to format."
2274 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002275 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2276 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002277 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002278 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002279 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2280 'clang-format-diff.py')
2281 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002282 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002283 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002284
2285 # Newer versions of clang-format-diff.py require an explicit -i flag
2286 # to apply the edits to files, otherwise it just displays a diff.
2287 # Probe the usage string to verify if this is needed.
2288 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2289 if '[-i]' in help_text:
2290 cmd.append('-i')
2291
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002292 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002293
2294 return 0
2295
2296
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002297class OptionParser(optparse.OptionParser):
2298 """Creates the option parse and add --verbose support."""
2299 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002300 optparse.OptionParser.__init__(
2301 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002302 self.add_option(
2303 '-v', '--verbose', action='count', default=0,
2304 help='Use 2 times for more debugging info')
2305
2306 def parse_args(self, args=None, values=None):
2307 options, args = optparse.OptionParser.parse_args(self, args, values)
2308 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2309 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2310 return options, args
2311
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002312
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002313def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002314 if sys.hexversion < 0x02060000:
2315 print >> sys.stderr, (
2316 '\nYour python version %s is unsupported, please upgrade.\n' %
2317 sys.version.split(' ', 1)[0])
2318 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002319
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002320 # Reload settings.
2321 global settings
2322 settings = Settings()
2323
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002324 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002325 dispatcher = subcommand.CommandDispatcher(__name__)
2326 try:
2327 return dispatcher.execute(OptionParser(), argv)
2328 except urllib2.HTTPError, e:
2329 if e.code != 500:
2330 raise
2331 DieWithError(
2332 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2333 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334
2335
2336if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002337 # These affect sys.stdout so do it outside of main() to simplify mocks in
2338 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002339 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002340 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002341 sys.exit(main(sys.argv[1:]))