blob: 5e166324f10d66ba82eb5703c94eeca4f17692b5 [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.orge8077812012-02-03 03:41:46 +00001022 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
1023 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
1024 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1027 #should be of the form
1028 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1029 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1030 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1031 keyvals['ORIGIN_URL_CONFIG']])
1032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001034def urlretrieve(source, destination):
1035 """urllib is broken for SSL connections via a proxy therefore we
1036 can't use urllib.urlretrieve()."""
1037 with open(destination, 'w') as f:
1038 f.write(urllib2.urlopen(source).read())
1039
1040
ukai@chromium.org712d6102013-11-27 00:52:58 +00001041def hasSheBang(fname):
1042 """Checks fname is a #! script."""
1043 with open(fname) as f:
1044 return f.read(2).startswith('#!')
1045
1046
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001047def DownloadHooks(force):
1048 """downloads hooks
1049
1050 Args:
1051 force: True to update hooks. False to install hooks if not present.
1052 """
1053 if not settings.GetIsGerrit():
1054 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001055 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001056 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1057 if not os.access(dst, os.X_OK):
1058 if os.path.exists(dst):
1059 if not force:
1060 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001061 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001062 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001063 if not hasSheBang(dst):
1064 DieWithError('Not a script: %s\n'
1065 'You need to download from\n%s\n'
1066 'into .git/hooks/commit-msg and '
1067 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001068 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1069 except Exception:
1070 if os.path.exists(dst):
1071 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001072 DieWithError('\nFailed to download hooks.\n'
1073 'You need to download from\n%s\n'
1074 'into .git/hooks/commit-msg and '
1075 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001076
1077
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001078@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001079def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001080 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001082 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 if len(args) == 0:
1084 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001085 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086 return 0
1087
1088 url = args[0]
1089 if not url.endswith('codereview.settings'):
1090 url = os.path.join(url, 'codereview.settings')
1091
1092 # Load code review settings and download hooks (if available).
1093 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001094 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 return 0
1096
1097
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001098def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001099 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001100 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1101 branch = ShortBranchName(branchref)
1102 _, args = parser.parse_args(args)
1103 if not args:
1104 print("Current base-url:")
1105 return RunGit(['config', 'branch.%s.base-url' % branch],
1106 error_ok=False).strip()
1107 else:
1108 print("Setting base-url to %s" % args[0])
1109 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1110 error_ok=False).strip()
1111
1112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001114 """Show status of changelists.
1115
1116 Colors are used to tell the state of the CL unless --fast is used:
1117 - Green LGTM'ed
1118 - Blue waiting for review
1119 - Yellow waiting for you to reply to review
1120 - Red not sent for review or broken
1121 - Cyan was committed, branch can be deleted
1122
1123 Also see 'git cl comments'.
1124 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 parser.add_option('--field',
1126 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001127 parser.add_option('-f', '--fast', action='store_true',
1128 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001130 if args:
1131 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001134 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 if options.field.startswith('desc'):
1136 print cl.GetDescription()
1137 elif options.field == 'id':
1138 issueid = cl.GetIssue()
1139 if issueid:
1140 print issueid
1141 elif options.field == 'patch':
1142 patchset = cl.GetPatchset()
1143 if patchset:
1144 print patchset
1145 elif options.field == 'url':
1146 url = cl.GetIssueURL()
1147 if url:
1148 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001149 return 0
1150
1151 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1152 if not branches:
1153 print('No local branch found.')
1154 return 0
1155
1156 changes = (Changelist(branchref=b) for b in branches.splitlines())
1157 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1158 alignment = max(5, max(len(b) for b in branches))
1159 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 # Adhoc thread pool to request data concurrently.
1161 output = Queue.Queue()
1162
1163 # Silence upload.py otherwise it becomes unweldly.
1164 upload.verbosity = 0
1165
1166 if not options.fast:
1167 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001168 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001169 c = Changelist(branchref=b)
1170 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001171 props = {}
1172 r = None
1173 if i:
1174 try:
1175 props = c.GetIssueProperties()
1176 r = c.GetApprovingReviewers() if i else None
1177 except urllib2.HTTPError:
1178 # The issue probably doesn't exist anymore.
1179 i += ' (broken)'
1180
1181 msgs = props.get('messages') or []
1182
1183 if not i:
1184 color = Fore.WHITE
1185 elif props.get('closed'):
1186 # Issue is closed.
1187 color = Fore.CYAN
1188 elif r:
1189 # Was LGTM'ed.
1190 color = Fore.GREEN
1191 elif not msgs:
1192 # No message was sent.
1193 color = Fore.RED
1194 elif msgs[-1]['sender'] != props.get('owner_email'):
1195 color = Fore.YELLOW
1196 else:
1197 color = Fore.BLUE
1198 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001199
1200 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1201 for t in threads:
1202 t.daemon = True
1203 t.start()
1204 else:
1205 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1206 for b in branches:
1207 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001208 url = c.GetIssueURL()
1209 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001210
1211 tmp = {}
1212 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001213 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001214 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001215 b, i, color = output.get()
1216 tmp[b] = (i, color)
1217 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001218 reset = Fore.RESET
1219 if not sys.stdout.isatty():
1220 color = ''
1221 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001222 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001223 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001225 cl = Changelist()
1226 print
1227 print 'Current branch:',
1228 if not cl.GetIssue():
1229 print 'no issue assigned.'
1230 return 0
1231 print cl.GetBranch()
1232 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1233 print 'Issue description:'
1234 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235 return 0
1236
1237
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001238def colorize_CMDstatus_doc():
1239 """To be called once in main() to add colors to git cl status help."""
1240 colors = [i for i in dir(Fore) if i[0].isupper()]
1241
1242 def colorize_line(line):
1243 for color in colors:
1244 if color in line.upper():
1245 # Extract whitespaces first and the leading '-'.
1246 indent = len(line) - len(line.lstrip(' ')) + 1
1247 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1248 return line
1249
1250 lines = CMDstatus.__doc__.splitlines()
1251 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1252
1253
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001254@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001256 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257
1258 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001259 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001260 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261
1262 cl = Changelist()
1263 if len(args) > 0:
1264 try:
1265 issue = int(args[0])
1266 except ValueError:
1267 DieWithError('Pass a number to set the issue or none to list it.\n'
1268 'Maybe you want to run git cl status?')
1269 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001270 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 return 0
1272
1273
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001274def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001275 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001276 (_, args) = parser.parse_args(args)
1277 if args:
1278 parser.error('Unsupported argument: %s' % args)
1279
1280 cl = Changelist()
1281 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001283 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001284 if message['disapproval']:
1285 color = Fore.RED
1286 elif message['approval']:
1287 color = Fore.GREEN
1288 elif message['sender'] == data['owner_email']:
1289 color = Fore.MAGENTA
1290 else:
1291 color = Fore.BLUE
1292 print '\n%s%s %s%s' % (
1293 color, message['date'].split('.', 1)[0], message['sender'],
1294 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001295 if message['text'].strip():
1296 print '\n'.join(' ' + l for l in message['text'].splitlines())
1297 return 0
1298
1299
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001300def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001301 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001302 cl = Changelist()
1303 if not cl.GetIssue():
1304 DieWithError('This branch has no associated changelist.')
1305 description = ChangeDescription(cl.GetDescription())
1306 description.prompt()
1307 cl.UpdateDescription(description.description)
1308 return 0
1309
1310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311def CreateDescriptionFromLog(args):
1312 """Pulls out the commit log to use as a base for the CL description."""
1313 log_args = []
1314 if len(args) == 1 and not args[0].endswith('.'):
1315 log_args = [args[0] + '..']
1316 elif len(args) == 1 and args[0].endswith('...'):
1317 log_args = [args[0][:-1]]
1318 elif len(args) == 2:
1319 log_args = [args[0] + '..' + args[1]]
1320 else:
1321 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001322 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323
1324
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001326 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001327 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001329 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001330 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 (options, args) = parser.parse_args(args)
1332
ukai@chromium.org259e4682012-10-25 07:36:33 +00001333 if not options.force and is_dirty_git_tree('presubmit'):
1334 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335 return 1
1336
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001337 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 if args:
1339 base_branch = args[0]
1340 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001341 # Default to diffing against the common ancestor of the upstream branch.
1342 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001344 cl.RunHook(
1345 committing=not options.upload,
1346 may_prompt=False,
1347 verbose=options.verbose,
1348 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001349 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
1351
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001352def AddChangeIdToCommitMessage(options, args):
1353 """Re-commits using the current message, assumes the commit hook is in
1354 place.
1355 """
1356 log_desc = options.message or CreateDescriptionFromLog(args)
1357 git_command = ['commit', '--amend', '-m', log_desc]
1358 RunGit(git_command)
1359 new_log_desc = CreateDescriptionFromLog(args)
1360 if CHANGE_ID in new_log_desc:
1361 print 'git-cl: Added Change-Id to commit message.'
1362 else:
1363 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1364
1365
ukai@chromium.orge8077812012-02-03 03:41:46 +00001366def GerritUpload(options, args, cl):
1367 """upload the current branch to gerrit."""
1368 # We assume the remote called "origin" is the one we want.
1369 # It is probably not worthwhile to support different workflows.
1370 remote = 'origin'
1371 branch = 'master'
1372 if options.target_branch:
1373 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001375 change_desc = ChangeDescription(
1376 options.message or CreateDescriptionFromLog(args))
1377 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001378 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001380 if CHANGE_ID not in change_desc.description:
1381 AddChangeIdToCommitMessage(options, args)
1382 if options.reviewers:
1383 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385 receive_options = []
1386 cc = cl.GetCCList().split(',')
1387 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001388 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001389 cc = filter(None, cc)
1390 if cc:
1391 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001392 if change_desc.get_reviewers():
1393 receive_options.extend(
1394 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
ukai@chromium.orge8077812012-02-03 03:41:46 +00001396 git_command = ['push']
1397 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001398 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001399 ' '.join(receive_options))
1400 git_command += [remote, 'HEAD:refs/for/' + branch]
1401 RunGit(git_command)
1402 # TODO(ukai): parse Change-Id: and set issue number?
1403 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405
ukai@chromium.orge8077812012-02-03 03:41:46 +00001406def RietveldUpload(options, args, cl):
1407 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1409 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410 if options.emulate_svn_auto_props:
1411 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412
1413 change_desc = None
1414
1415 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001416 if options.title:
1417 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001418 if options.message:
1419 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001420 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421 print ("This branch is associated with issue %s. "
1422 "Adding patch to that issue." % cl.GetIssue())
1423 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001424 if options.title:
1425 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001426 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001427 change_desc = ChangeDescription(message)
1428 if options.reviewers:
1429 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001430 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001431 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001432
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001433 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 print "Description is empty; aborting."
1435 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001436
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001437 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001438 if change_desc.get_reviewers():
1439 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001440 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001441 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001442 DieWithError("Must specify reviewers to send email.")
1443 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001444
1445 # We check this before applying rietveld.private assuming that in
1446 # rietveld.cc only addresses which we can send private CLs to are listed
1447 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1448 # --private is specified explicitly on the command line.
1449 if options.private:
1450 logging.warn('rietveld.cc is ignored since private flag is specified. '
1451 'You need to review and add them manually if necessary.')
1452 cc = cl.GetCCListWithoutDefault()
1453 else:
1454 cc = cl.GetCCList()
1455 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001456 if cc:
1457 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001459 if options.private or settings.GetDefaultPrivateFlag() == "True":
1460 upload_args.append('--private')
1461
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001462 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001463 if not options.find_copies:
1464 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001465
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 # Include the upstream repo's URL in the change -- this is useful for
1467 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001468 remote_url = cl.GetGitBaseUrlFromConfig()
1469 if not remote_url:
1470 if settings.GetIsGitSvn():
1471 # URL is dependent on the current directory.
1472 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1473 if data:
1474 keys = dict(line.split(': ', 1) for line in data.splitlines()
1475 if ': ' in line)
1476 remote_url = keys.get('URL', None)
1477 else:
1478 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1479 remote_url = (cl.GetRemoteUrl() + '@'
1480 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481 if remote_url:
1482 upload_args.extend(['--base_url', remote_url])
1483
1484 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001485 upload_args = ['upload'] + upload_args + args
1486 logging.info('upload.RealMain(%s)', upload_args)
1487 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001488 issue = int(issue)
1489 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001490 except KeyboardInterrupt:
1491 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 except:
1493 # If we got an exception after the user typed a description for their
1494 # change, back up the description before re-raising.
1495 if change_desc:
1496 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1497 print '\nGot exception while uploading -- saving description to %s\n' \
1498 % backup_path
1499 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001500 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 backup_file.close()
1502 raise
1503
1504 if not cl.GetIssue():
1505 cl.SetIssue(issue)
1506 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001507
1508 if options.use_commit_queue:
1509 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510 return 0
1511
1512
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001513def cleanup_list(l):
1514 """Fixes a list so that comma separated items are put as individual items.
1515
1516 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1517 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1518 """
1519 items = sum((i.split(',') for i in l), [])
1520 stripped_items = (i.strip() for i in items)
1521 return sorted(filter(None, stripped_items))
1522
1523
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001524@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001525def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001526 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001527 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1528 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001529 parser.add_option('--bypass-watchlists', action='store_true',
1530 dest='bypass_watchlists',
1531 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001532 parser.add_option('-f', action='store_true', dest='force',
1533 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001534 parser.add_option('-m', dest='message', help='message for patchset')
1535 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001536 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001537 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001538 help='reviewer email addresses')
1539 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001540 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001542 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001543 help='send email to reviewer immediately')
1544 parser.add_option("--emulate_svn_auto_props", action="store_true",
1545 dest="emulate_svn_auto_props",
1546 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547 parser.add_option('-c', '--use-commit-queue', action='store_true',
1548 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001549 parser.add_option('--private', action='store_true',
1550 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001551 parser.add_option('--target_branch',
1552 help='When uploading to gerrit, remote branch to '
1553 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001554 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001555 (options, args) = parser.parse_args(args)
1556
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001557 if options.target_branch and not settings.GetIsGerrit():
1558 parser.error('Use --target_branch for non gerrit repository.')
1559
ukai@chromium.org259e4682012-10-25 07:36:33 +00001560 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001561 return 1
1562
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001563 options.reviewers = cleanup_list(options.reviewers)
1564 options.cc = cleanup_list(options.cc)
1565
ukai@chromium.orge8077812012-02-03 03:41:46 +00001566 cl = Changelist()
1567 if args:
1568 # TODO(ukai): is it ok for gerrit case?
1569 base_branch = args[0]
1570 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001571 # Default to diffing against common ancestor of upstream branch
1572 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001573 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001574
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001575 # Apply watchlists on upload.
1576 change = cl.GetChange(base_branch, None)
1577 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1578 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001579 if not options.bypass_watchlists:
1580 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001581
ukai@chromium.orge8077812012-02-03 03:41:46 +00001582 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001583 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001584 may_prompt=not options.force,
1585 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001586 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001587 if not hook_results.should_continue():
1588 return 1
1589 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001590 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001591
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001592 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001593 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001594 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001595 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001596 print ('The last upload made from this repository was patchset #%d but '
1597 'the most recent patchset on the server is #%d.'
1598 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001599 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1600 'from another machine or branch the patch you\'re uploading now '
1601 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001602 ask_for_data('About to upload; enter to confirm.')
1603
iannucci@chromium.org79540052012-10-19 23:15:26 +00001604 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001605 if settings.GetIsGerrit():
1606 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001607 ret = RietveldUpload(options, args, cl)
1608 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001609 git_set_branch_value('last-upload-hash',
1610 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001611
1612 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001613
1614
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001615def IsSubmoduleMergeCommit(ref):
1616 # When submodules are added to the repo, we expect there to be a single
1617 # non-git-svn merge commit at remote HEAD with a signature comment.
1618 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001619 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001620 return RunGit(cmd) != ''
1621
1622
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623def SendUpstream(parser, args, cmd):
1624 """Common code for CmdPush and CmdDCommit
1625
1626 Squashed commit into a single.
1627 Updates changelog with metadata (e.g. pointer to review).
1628 Pushes/dcommits the code upstream.
1629 Updates review and closes.
1630 """
1631 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1632 help='bypass upload presubmit hook')
1633 parser.add_option('-m', dest='message',
1634 help="override review description")
1635 parser.add_option('-f', action='store_true', dest='force',
1636 help="force yes to questions (don't prompt)")
1637 parser.add_option('-c', dest='contributor',
1638 help="external contributor for patch (appended to " +
1639 "description and used as author for git). Should be " +
1640 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001641 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 (options, args) = parser.parse_args(args)
1643 cl = Changelist()
1644
1645 if not args or cmd == 'push':
1646 # Default to merging against our best guess of the upstream branch.
1647 args = [cl.GetUpstreamBranch()]
1648
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001649 if options.contributor:
1650 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1651 print "Please provide contibutor as 'First Last <email@example.com>'"
1652 return 1
1653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001655 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656
ukai@chromium.org259e4682012-10-25 07:36:33 +00001657 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001658 return 1
1659
1660 # This rev-list syntax means "show all commits not in my branch that
1661 # are in base_branch".
1662 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1663 base_branch]).splitlines()
1664 if upstream_commits:
1665 print ('Base branch "%s" has %d commits '
1666 'not in this branch.' % (base_branch, len(upstream_commits)))
1667 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1668 return 1
1669
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001670 # This is the revision `svn dcommit` will commit on top of.
1671 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1672 '--pretty=format:%H'])
1673
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001675 # If the base_head is a submodule merge commit, the first parent of the
1676 # base_head should be a git-svn commit, which is what we're interested in.
1677 base_svn_head = base_branch
1678 if base_has_submodules:
1679 base_svn_head += '^1'
1680
1681 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 if extra_commits:
1683 print ('This branch has %d additional commits not upstreamed yet.'
1684 % len(extra_commits.splitlines()))
1685 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1686 'before attempting to %s.' % (base_branch, cmd))
1687 return 1
1688
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001689 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001690 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001691 author = None
1692 if options.contributor:
1693 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001694 hook_results = cl.RunHook(
1695 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001696 may_prompt=not options.force,
1697 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001698 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001699 if not hook_results.should_continue():
1700 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701
1702 if cmd == 'dcommit':
1703 # Check the tree status if the tree status URL is set.
1704 status = GetTreeStatus()
1705 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001706 print('The tree is closed. Please wait for it to reopen. Use '
1707 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708 return 1
1709 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001710 print('Unable to determine tree status. Please verify manually and '
1711 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001712 else:
1713 breakpad.SendStack(
1714 'GitClHooksBypassedCommit',
1715 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001716 (cl.GetRietveldServer(), cl.GetIssue()),
1717 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001719 change_desc = ChangeDescription(options.message)
1720 if not change_desc.description and cl.GetIssue():
1721 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001723 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001724 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001725 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001726 else:
1727 print 'No description set.'
1728 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1729 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001731 # Keep a separate copy for the commit message, because the commit message
1732 # contains the link to the Rietveld issue, while the Rietveld message contains
1733 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001734 # Keep a separate copy for the commit message.
1735 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001736 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001737
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001738 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001739 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001740 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001741 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001742 commit_desc.append_footer('Patch from %s.' % options.contributor)
1743
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001744 print('Description:')
1745 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746
1747 branches = [base_branch, cl.GetBranchRef()]
1748 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001749 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001750 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001752 # We want to squash all this branch's commits into one commit with the proper
1753 # description. We do this by doing a "reset --soft" to the base branch (which
1754 # keeps the working copy the same), then dcommitting that. If origin/master
1755 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1756 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001757 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001758 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1759 # Delete the branches if they exist.
1760 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1761 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1762 result = RunGitWithCode(showref_cmd)
1763 if result[0] == 0:
1764 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765
1766 # We might be in a directory that's present in this branch but not in the
1767 # trunk. Move up to the top of the tree so that git commands that expect a
1768 # valid CWD won't fail after we check out the merge branch.
1769 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1770 if rel_base_path:
1771 os.chdir(rel_base_path)
1772
1773 # Stuff our change into the merge branch.
1774 # We wrap in a try...finally block so if anything goes wrong,
1775 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001776 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001778 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1779 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001781 RunGit(
1782 [
1783 'commit', '--author', options.contributor,
1784 '-m', commit_desc.description,
1785 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001787 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001788 if base_has_submodules:
1789 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1790 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1791 RunGit(['checkout', CHERRY_PICK_BRANCH])
1792 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001793 if cmd == 'push':
1794 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001795 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 retcode, output = RunGitWithCode(
1797 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1798 logging.debug(output)
1799 else:
1800 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001801 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001802 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001803 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001804 finally:
1805 # And then swap back to the original branch and clean up.
1806 RunGit(['checkout', '-q', cl.GetBranch()])
1807 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001808 if base_has_submodules:
1809 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810
1811 if cl.GetIssue():
1812 if cmd == 'dcommit' and 'Committed r' in output:
1813 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1814 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001815 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1816 for l in output.splitlines(False))
1817 match = filter(None, match)
1818 if len(match) != 1:
1819 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1820 output)
1821 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001822 else:
1823 return 1
1824 viewvc_url = settings.GetViewVCUrl()
1825 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001826 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001827 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001828 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 print ('Closing issue '
1830 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001831 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001833 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001834 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001835 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001836 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1837 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001838 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001839
1840 if retcode == 0:
1841 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1842 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001843 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 return 0
1846
1847
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001848@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001850 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001852 message = """This doesn't appear to be an SVN repository.
1853If your project has a git mirror with an upstream SVN master, you probably need
1854to run 'git svn init', see your project's git mirror documentation.
1855If your project has a true writeable upstream repository, you probably want
1856to run 'git cl push' instead.
1857Choose wisely, if you get this wrong, your commit might appear to succeed but
1858will instead be silently ignored."""
1859 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001860 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861 return SendUpstream(parser, args, 'dcommit')
1862
1863
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001864@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001866 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867 if settings.GetIsGitSvn():
1868 print('This appears to be an SVN repository.')
1869 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001870 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 return SendUpstream(parser, args, 'push')
1872
1873
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001874@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001876 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001877 parser.add_option('-b', dest='newbranch',
1878 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001879 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001881 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1882 help='Change to the directory DIR immediately, '
1883 'before doing anything else.')
1884 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001885 help='failed patches spew .rej files rather than '
1886 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1888 help="don't commit after patch applies")
1889 (options, args) = parser.parse_args(args)
1890 if len(args) != 1:
1891 parser.print_help()
1892 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001893 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001895 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001896 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001897
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001898 if options.newbranch:
1899 if options.force:
1900 RunGit(['branch', '-D', options.newbranch],
1901 stderr=subprocess2.PIPE, error_ok=True)
1902 RunGit(['checkout', '-b', options.newbranch,
1903 Changelist().GetUpstreamBranch()])
1904
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001905 return PatchIssue(issue_arg, options.reject, options.nocommit,
1906 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001907
1908
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001909def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001910 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001912 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001913 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001914 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001915 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001916 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001917 # Assume it's a URL to the patch. Default to https.
1918 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001919 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001920 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921 DieWithError('Must pass an issue ID or full URL for '
1922 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001923 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001924 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001925 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 # Switch up to the top-level directory, if necessary, in preparation for
1928 # applying the patch.
1929 top = RunGit(['rev-parse', '--show-cdup']).strip()
1930 if top:
1931 os.chdir(top)
1932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 # Git patches have a/ at the beginning of source paths. We strip that out
1934 # with a sed script rather than the -p flag to patch so we can feed either
1935 # Git or svn-style patches into the same apply command.
1936 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001937 try:
1938 patch_data = subprocess2.check_output(
1939 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1940 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941 DieWithError('Git patch mungling failed.')
1942 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001943 env = os.environ.copy()
1944 # 'cat' is a magical git string that disables pagers on all platforms.
1945 env['GIT_PAGER'] = 'cat'
1946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947 # We use "git apply" to apply the patch instead of "patch" so that we can
1948 # pick up file adds.
1949 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001950 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001951 if directory:
1952 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001953 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001954 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001955 elif IsGitVersionAtLeast('1.7.12'):
1956 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001957 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001958 subprocess2.check_call(cmd, env=env,
1959 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001960 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001961 DieWithError('Failed to apply the patch')
1962
1963 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001964 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1966 cl = Changelist()
1967 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001968 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001969 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970 else:
1971 print "Patch applied to index."
1972 return 0
1973
1974
1975def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001976 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977 # Provide a wrapper for git svn rebase to help avoid accidental
1978 # git svn dcommit.
1979 # It's the only command that doesn't use parser at all since we just defer
1980 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001981 env = os.environ.copy()
1982 # 'cat' is a magical git string that disables pagers on all platforms.
1983 env['GIT_PAGER'] = 'cat'
1984
1985 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986
1987
1988def GetTreeStatus():
1989 """Fetches the tree status and returns either 'open', 'closed',
1990 'unknown' or 'unset'."""
1991 url = settings.GetTreeStatusUrl(error_ok=True)
1992 if url:
1993 status = urllib2.urlopen(url).read().lower()
1994 if status.find('closed') != -1 or status == '0':
1995 return 'closed'
1996 elif status.find('open') != -1 or status == '1':
1997 return 'open'
1998 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 return 'unset'
2000
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002def GetTreeStatusReason():
2003 """Fetches the tree status from a json url and returns the message
2004 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002005 url = settings.GetTreeStatusUrl()
2006 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007 connection = urllib2.urlopen(json_url)
2008 status = json.loads(connection.read())
2009 connection.close()
2010 return status['message']
2011
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002013def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002014 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002015 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016 status = GetTreeStatus()
2017 if 'unset' == status:
2018 print 'You must configure your tree status URL by running "git cl config".'
2019 return 2
2020
2021 print "The tree is %s" % status
2022 print
2023 print GetTreeStatusReason()
2024 if status != 'open':
2025 return 1
2026 return 0
2027
2028
maruel@chromium.org15192402012-09-06 12:38:29 +00002029def CMDtry(parser, args):
2030 """Triggers a try job through Rietveld."""
2031 group = optparse.OptionGroup(parser, "Try job options")
2032 group.add_option(
2033 "-b", "--bot", action="append",
2034 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2035 "times to specify multiple builders. ex: "
2036 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2037 "the try server waterfall for the builders name and the tests "
2038 "available. Can also be used to specify gtest_filter, e.g. "
2039 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2040 group.add_option(
2041 "-r", "--revision",
2042 help="Revision to use for the try job; default: the "
2043 "revision will be determined by the try server; see "
2044 "its waterfall for more info")
2045 group.add_option(
2046 "-c", "--clobber", action="store_true", default=False,
2047 help="Force a clobber before building; e.g. don't do an "
2048 "incremental build")
2049 group.add_option(
2050 "--project",
2051 help="Override which project to use. Projects are defined "
2052 "server-side to define what default bot set to use")
2053 group.add_option(
2054 "-t", "--testfilter", action="append", default=[],
2055 help=("Apply a testfilter to all the selected builders. Unless the "
2056 "builders configurations are similar, use multiple "
2057 "--bot <builder>:<test> arguments."))
2058 group.add_option(
2059 "-n", "--name", help="Try job name; default to current branch name")
2060 parser.add_option_group(group)
2061 options, args = parser.parse_args(args)
2062
2063 if args:
2064 parser.error('Unknown arguments: %s' % args)
2065
2066 cl = Changelist()
2067 if not cl.GetIssue():
2068 parser.error('Need to upload first')
2069
2070 if not options.name:
2071 options.name = cl.GetBranch()
2072
2073 # Process --bot and --testfilter.
2074 if not options.bot:
2075 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002076 change = cl.GetChange(
2077 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2078 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002079 options.bot = presubmit_support.DoGetTrySlaves(
2080 change,
2081 change.LocalPaths(),
2082 settings.GetRoot(),
2083 None,
2084 None,
2085 options.verbose,
2086 sys.stdout)
2087 if not options.bot:
2088 parser.error('No default try builder to try, use --bot')
2089
2090 builders_and_tests = {}
2091 for bot in options.bot:
2092 if ':' in bot:
2093 builder, tests = bot.split(':', 1)
2094 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2095 elif ',' in bot:
2096 parser.error('Specify one bot per --bot flag')
2097 else:
2098 builders_and_tests.setdefault(bot, []).append('defaulttests')
2099
2100 if options.testfilter:
2101 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2102 builders_and_tests = dict(
2103 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2104 if t != ['compile'])
2105
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002106 if any('triggered' in b for b in builders_and_tests):
2107 print >> sys.stderr, (
2108 'ERROR You are trying to send a job to a triggered bot. This type of'
2109 ' bot requires an\ninitial job from a parent (usually a builder). '
2110 'Instead send your job to the parent.\n'
2111 'Bot list: %s' % builders_and_tests)
2112 return 1
2113
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002114 patchset = cl.GetMostRecentPatchset()
2115 if patchset and patchset != cl.GetPatchset():
2116 print(
2117 '\nWARNING Mismatch between local config and server. Did a previous '
2118 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2119 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002120
2121 cl.RpcServer().trigger_try_jobs(
2122 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2123 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002124 print('Tried jobs on:')
2125 length = max(len(builder) for builder in builders_and_tests)
2126 for builder in sorted(builders_and_tests):
2127 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002128 return 0
2129
2130
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002131@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002132def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002133 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002134 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002135 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002136 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002137 return 0
2138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002139 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002140 if args:
2141 # One arg means set upstream branch.
2142 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2143 cl = Changelist()
2144 print "Upstream branch set to " + cl.GetUpstreamBranch()
2145 else:
2146 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147 return 0
2148
2149
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002150def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002151 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002152 _, args = parser.parse_args(args)
2153 if args:
2154 parser.error('Unrecognized args: %s' % ' '.join(args))
2155 cl = Changelist()
2156 cl.SetFlag('commit', '1')
2157 return 0
2158
2159
groby@chromium.org411034a2013-02-26 15:12:01 +00002160def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002161 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002162 _, args = parser.parse_args(args)
2163 if args:
2164 parser.error('Unrecognized args: %s' % ' '.join(args))
2165 cl = Changelist()
2166 # Ensure there actually is an issue to close.
2167 cl.GetDescription()
2168 cl.CloseIssue()
2169 return 0
2170
2171
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002172def CMDdiff(parser, args):
2173 """shows differences between local tree and last upload."""
2174 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002175 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002176 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002177 if not issue:
2178 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002179 TMP_BRANCH = 'git-cl-diff'
2180 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2181
2182 # Create a new branch based on the merge-base
2183 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2184 try:
2185 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002186 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002187 if rtn != 0:
2188 return rtn
2189
2190 # Switch back to starting brand and diff against the temporary
2191 # branch containing the latest rietveld patch.
2192 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2193 finally:
2194 RunGit(['checkout', '-q', branch])
2195 RunGit(['branch', '-D', TMP_BRANCH])
2196
2197 return 0
2198
2199
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002200def CMDowners(parser, args):
2201 """interactively find the owners for reviewing"""
2202 parser.add_option(
2203 '--no-color',
2204 action='store_true',
2205 help='Use this option to disable color output')
2206 options, args = parser.parse_args(args)
2207
2208 author = RunGit(['config', 'user.email']).strip() or None
2209
2210 cl = Changelist()
2211
2212 if args:
2213 if len(args) > 1:
2214 parser.error('Unknown args')
2215 base_branch = args[0]
2216 else:
2217 # Default to diffing against the common ancestor of the upstream branch.
2218 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2219
2220 change = cl.GetChange(base_branch, None)
2221 return owners_finder.OwnersFinder(
2222 [f.LocalPath() for f in
2223 cl.GetChange(base_branch, None).AffectedFiles()],
2224 change.RepositoryRoot(), author,
2225 fopen=file, os_path=os.path, glob=glob.glob,
2226 disable_color=options.no_color).run()
2227
2228
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002229def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002230 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002231 CLANG_EXTS = ['.cc', '.cpp', '.h']
2232 parser.add_option('--full', action='store_true', default=False)
2233 opts, args = parser.parse_args(args)
2234 if args:
2235 parser.error('Unrecognized args: %s' % ' '.join(args))
2236
digit@chromium.org29e47272013-05-17 17:01:46 +00002237 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002238 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002239 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002240 # Only list the names of modified files.
2241 diff_cmd.append('--name-only')
2242 else:
2243 # Only generate context-less patches.
2244 diff_cmd.append('-U0')
2245
2246 # Grab the merge-base commit, i.e. the upstream commit of the current
2247 # branch when it was created or the last time it was rebased. This is
2248 # to cover the case where the user may have called "git fetch origin",
2249 # moving the origin branch to a newer commit, but hasn't rebased yet.
2250 upstream_commit = None
2251 cl = Changelist()
2252 upstream_branch = cl.GetUpstreamBranch()
2253 if upstream_branch:
2254 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2255 upstream_commit = upstream_commit.strip()
2256
2257 if not upstream_commit:
2258 DieWithError('Could not find base commit for this branch. '
2259 'Are you in detached state?')
2260
2261 diff_cmd.append(upstream_commit)
2262
2263 # Handle source file filtering.
2264 diff_cmd.append('--')
2265 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2266 diff_output = RunGit(diff_cmd)
2267
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002268 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2269
digit@chromium.org29e47272013-05-17 17:01:46 +00002270 if opts.full:
2271 # diff_output is a list of files to send to clang-format.
2272 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002273 if not files:
2274 print "Nothing to format."
2275 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002276 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2277 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002278 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002279 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002280 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2281 'clang-format-diff.py')
2282 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002283 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002284 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002285
2286 # Newer versions of clang-format-diff.py require an explicit -i flag
2287 # to apply the edits to files, otherwise it just displays a diff.
2288 # Probe the usage string to verify if this is needed.
2289 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2290 if '[-i]' in help_text:
2291 cmd.append('-i')
2292
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002293 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002294
2295 return 0
2296
2297
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002298class OptionParser(optparse.OptionParser):
2299 """Creates the option parse and add --verbose support."""
2300 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002301 optparse.OptionParser.__init__(
2302 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002303 self.add_option(
2304 '-v', '--verbose', action='count', default=0,
2305 help='Use 2 times for more debugging info')
2306
2307 def parse_args(self, args=None, values=None):
2308 options, args = optparse.OptionParser.parse_args(self, args, values)
2309 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2310 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2311 return options, args
2312
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002313
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002314def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002315 if sys.hexversion < 0x02060000:
2316 print >> sys.stderr, (
2317 '\nYour python version %s is unsupported, please upgrade.\n' %
2318 sys.version.split(' ', 1)[0])
2319 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002320
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002321 # Reload settings.
2322 global settings
2323 settings = Settings()
2324
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002325 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002326 dispatcher = subcommand.CommandDispatcher(__name__)
2327 try:
2328 return dispatcher.execute(OptionParser(), argv)
2329 except urllib2.HTTPError, e:
2330 if e.code != 500:
2331 raise
2332 DieWithError(
2333 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2334 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002335
2336
2337if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002338 # These affect sys.stdout so do it outside of main() to simplify mocks in
2339 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002340 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002341 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002342 sys.exit(main(sys.argv[1:]))