blob: 03bd879cc68db86b430a0fb4386284cec3e997b1 [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.org78c4b982012-02-14 02:20:26 +00001041def DownloadHooks(force):
1042 """downloads hooks
1043
1044 Args:
1045 force: True to update hooks. False to install hooks if not present.
1046 """
1047 if not settings.GetIsGerrit():
1048 return
1049 server_url = settings.GetDefaultServerUrl()
1050 src = '%s/tools/hooks/commit-msg' % server_url
1051 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1052 if not os.access(dst, os.X_OK):
1053 if os.path.exists(dst):
1054 if not force:
1055 return
1056 os.remove(dst)
1057 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001058 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001059 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1060 except Exception:
1061 if os.path.exists(dst):
1062 os.remove(dst)
1063 DieWithError('\nFailed to download hooks from %s' % src)
1064
1065
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001066@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001068 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001070 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 if len(args) == 0:
1072 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001073 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074 return 0
1075
1076 url = args[0]
1077 if not url.endswith('codereview.settings'):
1078 url = os.path.join(url, 'codereview.settings')
1079
1080 # Load code review settings and download hooks (if available).
1081 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001082 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 return 0
1084
1085
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001086def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001087 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001088 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1089 branch = ShortBranchName(branchref)
1090 _, args = parser.parse_args(args)
1091 if not args:
1092 print("Current base-url:")
1093 return RunGit(['config', 'branch.%s.base-url' % branch],
1094 error_ok=False).strip()
1095 else:
1096 print("Setting base-url to %s" % args[0])
1097 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1098 error_ok=False).strip()
1099
1100
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001102 """Show status of changelists.
1103
1104 Colors are used to tell the state of the CL unless --fast is used:
1105 - Green LGTM'ed
1106 - Blue waiting for review
1107 - Yellow waiting for you to reply to review
1108 - Red not sent for review or broken
1109 - Cyan was committed, branch can be deleted
1110
1111 Also see 'git cl comments'.
1112 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113 parser.add_option('--field',
1114 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001115 parser.add_option('-f', '--fast', action='store_true',
1116 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001118 if args:
1119 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001122 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 if options.field.startswith('desc'):
1124 print cl.GetDescription()
1125 elif options.field == 'id':
1126 issueid = cl.GetIssue()
1127 if issueid:
1128 print issueid
1129 elif options.field == 'patch':
1130 patchset = cl.GetPatchset()
1131 if patchset:
1132 print patchset
1133 elif options.field == 'url':
1134 url = cl.GetIssueURL()
1135 if url:
1136 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001137 return 0
1138
1139 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1140 if not branches:
1141 print('No local branch found.')
1142 return 0
1143
1144 changes = (Changelist(branchref=b) for b in branches.splitlines())
1145 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1146 alignment = max(5, max(len(b) for b in branches))
1147 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001148 # Adhoc thread pool to request data concurrently.
1149 output = Queue.Queue()
1150
1151 # Silence upload.py otherwise it becomes unweldly.
1152 upload.verbosity = 0
1153
1154 if not options.fast:
1155 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001156 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001157 c = Changelist(branchref=b)
1158 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001159 props = {}
1160 r = None
1161 if i:
1162 try:
1163 props = c.GetIssueProperties()
1164 r = c.GetApprovingReviewers() if i else None
1165 except urllib2.HTTPError:
1166 # The issue probably doesn't exist anymore.
1167 i += ' (broken)'
1168
1169 msgs = props.get('messages') or []
1170
1171 if not i:
1172 color = Fore.WHITE
1173 elif props.get('closed'):
1174 # Issue is closed.
1175 color = Fore.CYAN
1176 elif r:
1177 # Was LGTM'ed.
1178 color = Fore.GREEN
1179 elif not msgs:
1180 # No message was sent.
1181 color = Fore.RED
1182 elif msgs[-1]['sender'] != props.get('owner_email'):
1183 color = Fore.YELLOW
1184 else:
1185 color = Fore.BLUE
1186 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001187
1188 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1189 for t in threads:
1190 t.daemon = True
1191 t.start()
1192 else:
1193 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1194 for b in branches:
1195 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001196 url = c.GetIssueURL()
1197 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001198
1199 tmp = {}
1200 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001201 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001202 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001203 b, i, color = output.get()
1204 tmp[b] = (i, color)
1205 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001206 reset = Fore.RESET
1207 if not sys.stdout.isatty():
1208 color = ''
1209 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001210 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001211 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001212
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001213 cl = Changelist()
1214 print
1215 print 'Current branch:',
1216 if not cl.GetIssue():
1217 print 'no issue assigned.'
1218 return 0
1219 print cl.GetBranch()
1220 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1221 print 'Issue description:'
1222 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 return 0
1224
1225
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001226def colorize_CMDstatus_doc():
1227 """To be called once in main() to add colors to git cl status help."""
1228 colors = [i for i in dir(Fore) if i[0].isupper()]
1229
1230 def colorize_line(line):
1231 for color in colors:
1232 if color in line.upper():
1233 # Extract whitespaces first and the leading '-'.
1234 indent = len(line) - len(line.lstrip(' ')) + 1
1235 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1236 return line
1237
1238 lines = CMDstatus.__doc__.splitlines()
1239 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1240
1241
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001242@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001244 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245
1246 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001247 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001248 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249
1250 cl = Changelist()
1251 if len(args) > 0:
1252 try:
1253 issue = int(args[0])
1254 except ValueError:
1255 DieWithError('Pass a number to set the issue or none to list it.\n'
1256 'Maybe you want to run git cl status?')
1257 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001258 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 return 0
1260
1261
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001262def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001263 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001264 (_, args) = parser.parse_args(args)
1265 if args:
1266 parser.error('Unsupported argument: %s' % args)
1267
1268 cl = Changelist()
1269 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001270 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001271 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001272 if message['disapproval']:
1273 color = Fore.RED
1274 elif message['approval']:
1275 color = Fore.GREEN
1276 elif message['sender'] == data['owner_email']:
1277 color = Fore.MAGENTA
1278 else:
1279 color = Fore.BLUE
1280 print '\n%s%s %s%s' % (
1281 color, message['date'].split('.', 1)[0], message['sender'],
1282 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001283 if message['text'].strip():
1284 print '\n'.join(' ' + l for l in message['text'].splitlines())
1285 return 0
1286
1287
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001288def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001289 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001290 cl = Changelist()
1291 if not cl.GetIssue():
1292 DieWithError('This branch has no associated changelist.')
1293 description = ChangeDescription(cl.GetDescription())
1294 description.prompt()
1295 cl.UpdateDescription(description.description)
1296 return 0
1297
1298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299def CreateDescriptionFromLog(args):
1300 """Pulls out the commit log to use as a base for the CL description."""
1301 log_args = []
1302 if len(args) == 1 and not args[0].endswith('.'):
1303 log_args = [args[0] + '..']
1304 elif len(args) == 1 and args[0].endswith('...'):
1305 log_args = [args[0][:-1]]
1306 elif len(args) == 2:
1307 log_args = [args[0] + '..' + args[1]]
1308 else:
1309 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001310 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311
1312
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001314 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001315 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001317 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001318 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319 (options, args) = parser.parse_args(args)
1320
ukai@chromium.org259e4682012-10-25 07:36:33 +00001321 if not options.force and is_dirty_git_tree('presubmit'):
1322 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323 return 1
1324
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001325 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326 if args:
1327 base_branch = args[0]
1328 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 # Default to diffing against the common ancestor of the upstream branch.
1330 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001332 cl.RunHook(
1333 committing=not options.upload,
1334 may_prompt=False,
1335 verbose=options.verbose,
1336 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001337 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338
1339
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001340def AddChangeIdToCommitMessage(options, args):
1341 """Re-commits using the current message, assumes the commit hook is in
1342 place.
1343 """
1344 log_desc = options.message or CreateDescriptionFromLog(args)
1345 git_command = ['commit', '--amend', '-m', log_desc]
1346 RunGit(git_command)
1347 new_log_desc = CreateDescriptionFromLog(args)
1348 if CHANGE_ID in new_log_desc:
1349 print 'git-cl: Added Change-Id to commit message.'
1350 else:
1351 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1352
1353
ukai@chromium.orge8077812012-02-03 03:41:46 +00001354def GerritUpload(options, args, cl):
1355 """upload the current branch to gerrit."""
1356 # We assume the remote called "origin" is the one we want.
1357 # It is probably not worthwhile to support different workflows.
1358 remote = 'origin'
1359 branch = 'master'
1360 if options.target_branch:
1361 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001363 change_desc = ChangeDescription(
1364 options.message or CreateDescriptionFromLog(args))
1365 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001366 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001368 if CHANGE_ID not in change_desc.description:
1369 AddChangeIdToCommitMessage(options, args)
1370 if options.reviewers:
1371 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372
ukai@chromium.orge8077812012-02-03 03:41:46 +00001373 receive_options = []
1374 cc = cl.GetCCList().split(',')
1375 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001376 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001377 cc = filter(None, cc)
1378 if cc:
1379 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001380 if change_desc.get_reviewers():
1381 receive_options.extend(
1382 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383
ukai@chromium.orge8077812012-02-03 03:41:46 +00001384 git_command = ['push']
1385 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001386 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001387 ' '.join(receive_options))
1388 git_command += [remote, 'HEAD:refs/for/' + branch]
1389 RunGit(git_command)
1390 # TODO(ukai): parse Change-Id: and set issue number?
1391 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
ukai@chromium.orge8077812012-02-03 03:41:46 +00001394def RietveldUpload(options, args, cl):
1395 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1397 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 if options.emulate_svn_auto_props:
1399 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400
1401 change_desc = None
1402
1403 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001404 if options.title:
1405 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001406 if options.message:
1407 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001408 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 print ("This branch is associated with issue %s. "
1410 "Adding patch to that issue." % cl.GetIssue())
1411 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001412 if options.title:
1413 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001414 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001415 change_desc = ChangeDescription(message)
1416 if options.reviewers:
1417 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001418 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001419 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001420
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001421 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 print "Description is empty; aborting."
1423 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001424
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001425 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001426 if change_desc.get_reviewers():
1427 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001428 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001429 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001430 DieWithError("Must specify reviewers to send email.")
1431 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001432
1433 # We check this before applying rietveld.private assuming that in
1434 # rietveld.cc only addresses which we can send private CLs to are listed
1435 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1436 # --private is specified explicitly on the command line.
1437 if options.private:
1438 logging.warn('rietveld.cc is ignored since private flag is specified. '
1439 'You need to review and add them manually if necessary.')
1440 cc = cl.GetCCListWithoutDefault()
1441 else:
1442 cc = cl.GetCCList()
1443 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001444 if cc:
1445 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001447 if options.private or settings.GetDefaultPrivateFlag() == "True":
1448 upload_args.append('--private')
1449
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001450 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001451 if not options.find_copies:
1452 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 # Include the upstream repo's URL in the change -- this is useful for
1455 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001456 remote_url = cl.GetGitBaseUrlFromConfig()
1457 if not remote_url:
1458 if settings.GetIsGitSvn():
1459 # URL is dependent on the current directory.
1460 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1461 if data:
1462 keys = dict(line.split(': ', 1) for line in data.splitlines()
1463 if ': ' in line)
1464 remote_url = keys.get('URL', None)
1465 else:
1466 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1467 remote_url = (cl.GetRemoteUrl() + '@'
1468 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469 if remote_url:
1470 upload_args.extend(['--base_url', remote_url])
1471
1472 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001473 upload_args = ['upload'] + upload_args + args
1474 logging.info('upload.RealMain(%s)', upload_args)
1475 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001476 issue = int(issue)
1477 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001478 except KeyboardInterrupt:
1479 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480 except:
1481 # If we got an exception after the user typed a description for their
1482 # change, back up the description before re-raising.
1483 if change_desc:
1484 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1485 print '\nGot exception while uploading -- saving description to %s\n' \
1486 % backup_path
1487 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001488 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489 backup_file.close()
1490 raise
1491
1492 if not cl.GetIssue():
1493 cl.SetIssue(issue)
1494 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001495
1496 if options.use_commit_queue:
1497 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498 return 0
1499
1500
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001501def cleanup_list(l):
1502 """Fixes a list so that comma separated items are put as individual items.
1503
1504 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1505 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1506 """
1507 items = sum((i.split(',') for i in l), [])
1508 stripped_items = (i.strip() for i in items)
1509 return sorted(filter(None, stripped_items))
1510
1511
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001512@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001513def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001514 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001515 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1516 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001517 parser.add_option('--bypass-watchlists', action='store_true',
1518 dest='bypass_watchlists',
1519 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001520 parser.add_option('-f', action='store_true', dest='force',
1521 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001522 parser.add_option('-m', dest='message', help='message for patchset')
1523 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001524 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001525 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001526 help='reviewer email addresses')
1527 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001528 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001529 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001530 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001531 help='send email to reviewer immediately')
1532 parser.add_option("--emulate_svn_auto_props", action="store_true",
1533 dest="emulate_svn_auto_props",
1534 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001535 parser.add_option('-c', '--use-commit-queue', action='store_true',
1536 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001537 parser.add_option('--private', action='store_true',
1538 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001539 parser.add_option('--target_branch',
1540 help='When uploading to gerrit, remote branch to '
1541 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001542 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001543 (options, args) = parser.parse_args(args)
1544
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001545 if options.target_branch and not settings.GetIsGerrit():
1546 parser.error('Use --target_branch for non gerrit repository.')
1547
ukai@chromium.org259e4682012-10-25 07:36:33 +00001548 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001549 return 1
1550
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001551 options.reviewers = cleanup_list(options.reviewers)
1552 options.cc = cleanup_list(options.cc)
1553
ukai@chromium.orge8077812012-02-03 03:41:46 +00001554 cl = Changelist()
1555 if args:
1556 # TODO(ukai): is it ok for gerrit case?
1557 base_branch = args[0]
1558 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001559 # Default to diffing against common ancestor of upstream branch
1560 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001561 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001562
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001563 # Apply watchlists on upload.
1564 change = cl.GetChange(base_branch, None)
1565 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1566 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001567 if not options.bypass_watchlists:
1568 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001569
ukai@chromium.orge8077812012-02-03 03:41:46 +00001570 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001571 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001572 may_prompt=not options.force,
1573 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001574 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001575 if not hook_results.should_continue():
1576 return 1
1577 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001578 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001579
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001580 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001581 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001582 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001583 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001584 print ('The last upload made from this repository was patchset #%d but '
1585 'the most recent patchset on the server is #%d.'
1586 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001587 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1588 'from another machine or branch the patch you\'re uploading now '
1589 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001590 ask_for_data('About to upload; enter to confirm.')
1591
iannucci@chromium.org79540052012-10-19 23:15:26 +00001592 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001593 if settings.GetIsGerrit():
1594 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001595 ret = RietveldUpload(options, args, cl)
1596 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001597 git_set_branch_value('last-upload-hash',
1598 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001599
1600 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001601
1602
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001603def IsSubmoduleMergeCommit(ref):
1604 # When submodules are added to the repo, we expect there to be a single
1605 # non-git-svn merge commit at remote HEAD with a signature comment.
1606 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001607 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001608 return RunGit(cmd) != ''
1609
1610
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611def SendUpstream(parser, args, cmd):
1612 """Common code for CmdPush and CmdDCommit
1613
1614 Squashed commit into a single.
1615 Updates changelog with metadata (e.g. pointer to review).
1616 Pushes/dcommits the code upstream.
1617 Updates review and closes.
1618 """
1619 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1620 help='bypass upload presubmit hook')
1621 parser.add_option('-m', dest='message',
1622 help="override review description")
1623 parser.add_option('-f', action='store_true', dest='force',
1624 help="force yes to questions (don't prompt)")
1625 parser.add_option('-c', dest='contributor',
1626 help="external contributor for patch (appended to " +
1627 "description and used as author for git). Should be " +
1628 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001629 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630 (options, args) = parser.parse_args(args)
1631 cl = Changelist()
1632
1633 if not args or cmd == 'push':
1634 # Default to merging against our best guess of the upstream branch.
1635 args = [cl.GetUpstreamBranch()]
1636
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001637 if options.contributor:
1638 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1639 print "Please provide contibutor as 'First Last <email@example.com>'"
1640 return 1
1641
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001643 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001644
ukai@chromium.org259e4682012-10-25 07:36:33 +00001645 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646 return 1
1647
1648 # This rev-list syntax means "show all commits not in my branch that
1649 # are in base_branch".
1650 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1651 base_branch]).splitlines()
1652 if upstream_commits:
1653 print ('Base branch "%s" has %d commits '
1654 'not in this branch.' % (base_branch, len(upstream_commits)))
1655 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1656 return 1
1657
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001658 # This is the revision `svn dcommit` will commit on top of.
1659 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1660 '--pretty=format:%H'])
1661
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001663 # If the base_head is a submodule merge commit, the first parent of the
1664 # base_head should be a git-svn commit, which is what we're interested in.
1665 base_svn_head = base_branch
1666 if base_has_submodules:
1667 base_svn_head += '^1'
1668
1669 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001670 if extra_commits:
1671 print ('This branch has %d additional commits not upstreamed yet.'
1672 % len(extra_commits.splitlines()))
1673 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1674 'before attempting to %s.' % (base_branch, cmd))
1675 return 1
1676
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001677 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001678 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001679 author = None
1680 if options.contributor:
1681 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001682 hook_results = cl.RunHook(
1683 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001684 may_prompt=not options.force,
1685 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001686 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001687 if not hook_results.should_continue():
1688 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689
1690 if cmd == 'dcommit':
1691 # Check the tree status if the tree status URL is set.
1692 status = GetTreeStatus()
1693 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001694 print('The tree is closed. Please wait for it to reopen. Use '
1695 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001696 return 1
1697 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001698 print('Unable to determine tree status. Please verify manually and '
1699 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001700 else:
1701 breakpad.SendStack(
1702 'GitClHooksBypassedCommit',
1703 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001704 (cl.GetRietveldServer(), cl.GetIssue()),
1705 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001707 change_desc = ChangeDescription(options.message)
1708 if not change_desc.description and cl.GetIssue():
1709 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001711 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001712 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001713 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001714 else:
1715 print 'No description set.'
1716 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1717 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001719 # Keep a separate copy for the commit message, because the commit message
1720 # contains the link to the Rietveld issue, while the Rietveld message contains
1721 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001722 # Keep a separate copy for the commit message.
1723 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001724 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001725
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001726 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001727 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001728 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001729 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001730 commit_desc.append_footer('Patch from %s.' % options.contributor)
1731
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001732 print('Description:')
1733 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001734
1735 branches = [base_branch, cl.GetBranchRef()]
1736 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001737 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001738 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001740 # We want to squash all this branch's commits into one commit with the proper
1741 # description. We do this by doing a "reset --soft" to the base branch (which
1742 # keeps the working copy the same), then dcommitting that. If origin/master
1743 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1744 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001746 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1747 # Delete the branches if they exist.
1748 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1749 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1750 result = RunGitWithCode(showref_cmd)
1751 if result[0] == 0:
1752 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753
1754 # We might be in a directory that's present in this branch but not in the
1755 # trunk. Move up to the top of the tree so that git commands that expect a
1756 # valid CWD won't fail after we check out the merge branch.
1757 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1758 if rel_base_path:
1759 os.chdir(rel_base_path)
1760
1761 # Stuff our change into the merge branch.
1762 # We wrap in a try...finally block so if anything goes wrong,
1763 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001764 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001766 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1767 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001769 RunGit(
1770 [
1771 'commit', '--author', options.contributor,
1772 '-m', commit_desc.description,
1773 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001774 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001775 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001776 if base_has_submodules:
1777 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1778 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1779 RunGit(['checkout', CHERRY_PICK_BRANCH])
1780 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781 if cmd == 'push':
1782 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001783 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784 retcode, output = RunGitWithCode(
1785 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1786 logging.debug(output)
1787 else:
1788 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001789 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001790 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001791 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001792 finally:
1793 # And then swap back to the original branch and clean up.
1794 RunGit(['checkout', '-q', cl.GetBranch()])
1795 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001796 if base_has_submodules:
1797 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001798
1799 if cl.GetIssue():
1800 if cmd == 'dcommit' and 'Committed r' in output:
1801 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1802 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001803 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1804 for l in output.splitlines(False))
1805 match = filter(None, match)
1806 if len(match) != 1:
1807 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1808 output)
1809 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810 else:
1811 return 1
1812 viewvc_url = settings.GetViewVCUrl()
1813 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001814 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001815 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001816 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817 print ('Closing issue '
1818 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001819 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001821 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001822 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001823 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001824 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1825 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001826 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001827
1828 if retcode == 0:
1829 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1830 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001831 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001832
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 return 0
1834
1835
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001836@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001837def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001838 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001840 message = """This doesn't appear to be an SVN repository.
1841If your project has a git mirror with an upstream SVN master, you probably need
1842to run 'git svn init', see your project's git mirror documentation.
1843If your project has a true writeable upstream repository, you probably want
1844to run 'git cl push' instead.
1845Choose wisely, if you get this wrong, your commit might appear to succeed but
1846will instead be silently ignored."""
1847 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001848 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 return SendUpstream(parser, args, 'dcommit')
1850
1851
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001852@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001854 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855 if settings.GetIsGitSvn():
1856 print('This appears to be an SVN repository.')
1857 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001858 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859 return SendUpstream(parser, args, 'push')
1860
1861
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001862@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001864 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865 parser.add_option('-b', dest='newbranch',
1866 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001867 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001868 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001869 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1870 help='Change to the directory DIR immediately, '
1871 'before doing anything else.')
1872 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001873 help='failed patches spew .rej files rather than '
1874 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1876 help="don't commit after patch applies")
1877 (options, args) = parser.parse_args(args)
1878 if len(args) != 1:
1879 parser.print_help()
1880 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001881 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001882
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001883 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001884 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001885
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001886 if options.newbranch:
1887 if options.force:
1888 RunGit(['branch', '-D', options.newbranch],
1889 stderr=subprocess2.PIPE, error_ok=True)
1890 RunGit(['checkout', '-b', options.newbranch,
1891 Changelist().GetUpstreamBranch()])
1892
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001893 return PatchIssue(issue_arg, options.reject, options.nocommit,
1894 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001895
1896
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001897def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001898 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001899 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001900 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001901 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001902 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001903 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001904 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001905 # Assume it's a URL to the patch. Default to https.
1906 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001907 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001908 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 DieWithError('Must pass an issue ID or full URL for '
1910 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001911 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001912 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001913 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001914
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915 # Switch up to the top-level directory, if necessary, in preparation for
1916 # applying the patch.
1917 top = RunGit(['rev-parse', '--show-cdup']).strip()
1918 if top:
1919 os.chdir(top)
1920
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921 # Git patches have a/ at the beginning of source paths. We strip that out
1922 # with a sed script rather than the -p flag to patch so we can feed either
1923 # Git or svn-style patches into the same apply command.
1924 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001925 try:
1926 patch_data = subprocess2.check_output(
1927 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1928 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929 DieWithError('Git patch mungling failed.')
1930 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001931 env = os.environ.copy()
1932 # 'cat' is a magical git string that disables pagers on all platforms.
1933 env['GIT_PAGER'] = 'cat'
1934
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001935 # We use "git apply" to apply the patch instead of "patch" so that we can
1936 # pick up file adds.
1937 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001938 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001939 if directory:
1940 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001941 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001943 elif IsGitVersionAtLeast('1.7.12'):
1944 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001945 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001946 subprocess2.check_call(cmd, env=env,
1947 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001948 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949 DieWithError('Failed to apply the patch')
1950
1951 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001952 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001953 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1954 cl = Changelist()
1955 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001956 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001957 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 else:
1959 print "Patch applied to index."
1960 return 0
1961
1962
1963def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001964 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965 # Provide a wrapper for git svn rebase to help avoid accidental
1966 # git svn dcommit.
1967 # It's the only command that doesn't use parser at all since we just defer
1968 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001969 env = os.environ.copy()
1970 # 'cat' is a magical git string that disables pagers on all platforms.
1971 env['GIT_PAGER'] = 'cat'
1972
1973 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974
1975
1976def GetTreeStatus():
1977 """Fetches the tree status and returns either 'open', 'closed',
1978 'unknown' or 'unset'."""
1979 url = settings.GetTreeStatusUrl(error_ok=True)
1980 if url:
1981 status = urllib2.urlopen(url).read().lower()
1982 if status.find('closed') != -1 or status == '0':
1983 return 'closed'
1984 elif status.find('open') != -1 or status == '1':
1985 return 'open'
1986 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987 return 'unset'
1988
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001989
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990def GetTreeStatusReason():
1991 """Fetches the tree status from a json url and returns the message
1992 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001993 url = settings.GetTreeStatusUrl()
1994 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995 connection = urllib2.urlopen(json_url)
1996 status = json.loads(connection.read())
1997 connection.close()
1998 return status['message']
1999
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002002 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002003 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004 status = GetTreeStatus()
2005 if 'unset' == status:
2006 print 'You must configure your tree status URL by running "git cl config".'
2007 return 2
2008
2009 print "The tree is %s" % status
2010 print
2011 print GetTreeStatusReason()
2012 if status != 'open':
2013 return 1
2014 return 0
2015
2016
maruel@chromium.org15192402012-09-06 12:38:29 +00002017def CMDtry(parser, args):
2018 """Triggers a try job through Rietveld."""
2019 group = optparse.OptionGroup(parser, "Try job options")
2020 group.add_option(
2021 "-b", "--bot", action="append",
2022 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2023 "times to specify multiple builders. ex: "
2024 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2025 "the try server waterfall for the builders name and the tests "
2026 "available. Can also be used to specify gtest_filter, e.g. "
2027 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2028 group.add_option(
2029 "-r", "--revision",
2030 help="Revision to use for the try job; default: the "
2031 "revision will be determined by the try server; see "
2032 "its waterfall for more info")
2033 group.add_option(
2034 "-c", "--clobber", action="store_true", default=False,
2035 help="Force a clobber before building; e.g. don't do an "
2036 "incremental build")
2037 group.add_option(
2038 "--project",
2039 help="Override which project to use. Projects are defined "
2040 "server-side to define what default bot set to use")
2041 group.add_option(
2042 "-t", "--testfilter", action="append", default=[],
2043 help=("Apply a testfilter to all the selected builders. Unless the "
2044 "builders configurations are similar, use multiple "
2045 "--bot <builder>:<test> arguments."))
2046 group.add_option(
2047 "-n", "--name", help="Try job name; default to current branch name")
2048 parser.add_option_group(group)
2049 options, args = parser.parse_args(args)
2050
2051 if args:
2052 parser.error('Unknown arguments: %s' % args)
2053
2054 cl = Changelist()
2055 if not cl.GetIssue():
2056 parser.error('Need to upload first')
2057
2058 if not options.name:
2059 options.name = cl.GetBranch()
2060
2061 # Process --bot and --testfilter.
2062 if not options.bot:
2063 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002064 change = cl.GetChange(
2065 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2066 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002067 options.bot = presubmit_support.DoGetTrySlaves(
2068 change,
2069 change.LocalPaths(),
2070 settings.GetRoot(),
2071 None,
2072 None,
2073 options.verbose,
2074 sys.stdout)
2075 if not options.bot:
2076 parser.error('No default try builder to try, use --bot')
2077
2078 builders_and_tests = {}
2079 for bot in options.bot:
2080 if ':' in bot:
2081 builder, tests = bot.split(':', 1)
2082 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2083 elif ',' in bot:
2084 parser.error('Specify one bot per --bot flag')
2085 else:
2086 builders_and_tests.setdefault(bot, []).append('defaulttests')
2087
2088 if options.testfilter:
2089 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2090 builders_and_tests = dict(
2091 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2092 if t != ['compile'])
2093
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002094 if any('triggered' in b for b in builders_and_tests):
2095 print >> sys.stderr, (
2096 'ERROR You are trying to send a job to a triggered bot. This type of'
2097 ' bot requires an\ninitial job from a parent (usually a builder). '
2098 'Instead send your job to the parent.\n'
2099 'Bot list: %s' % builders_and_tests)
2100 return 1
2101
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002102 patchset = cl.GetMostRecentPatchset()
2103 if patchset and patchset != cl.GetPatchset():
2104 print(
2105 '\nWARNING Mismatch between local config and server. Did a previous '
2106 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2107 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002108
2109 cl.RpcServer().trigger_try_jobs(
2110 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2111 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002112 print('Tried jobs on:')
2113 length = max(len(builder) for builder in builders_and_tests)
2114 for builder in sorted(builders_and_tests):
2115 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002116 return 0
2117
2118
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002119@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002121 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002122 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002123 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002124 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002125 return 0
2126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002128 if args:
2129 # One arg means set upstream branch.
2130 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2131 cl = Changelist()
2132 print "Upstream branch set to " + cl.GetUpstreamBranch()
2133 else:
2134 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002135 return 0
2136
2137
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002138def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002139 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002140 _, args = parser.parse_args(args)
2141 if args:
2142 parser.error('Unrecognized args: %s' % ' '.join(args))
2143 cl = Changelist()
2144 cl.SetFlag('commit', '1')
2145 return 0
2146
2147
groby@chromium.org411034a2013-02-26 15:12:01 +00002148def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002149 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002150 _, args = parser.parse_args(args)
2151 if args:
2152 parser.error('Unrecognized args: %s' % ' '.join(args))
2153 cl = Changelist()
2154 # Ensure there actually is an issue to close.
2155 cl.GetDescription()
2156 cl.CloseIssue()
2157 return 0
2158
2159
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002160def CMDdiff(parser, args):
2161 """shows differences between local tree and last upload."""
2162 cl = Changelist()
2163 branch = cl.GetBranch()
2164 TMP_BRANCH = 'git-cl-diff'
2165 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2166
2167 # Create a new branch based on the merge-base
2168 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2169 try:
2170 # Patch in the latest changes from rietveld.
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002171 rtn = PatchIssue(cl.GetIssue(), False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002172 if rtn != 0:
2173 return rtn
2174
2175 # Switch back to starting brand and diff against the temporary
2176 # branch containing the latest rietveld patch.
2177 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2178 finally:
2179 RunGit(['checkout', '-q', branch])
2180 RunGit(['branch', '-D', TMP_BRANCH])
2181
2182 return 0
2183
2184
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002185def CMDowners(parser, args):
2186 """interactively find the owners for reviewing"""
2187 parser.add_option(
2188 '--no-color',
2189 action='store_true',
2190 help='Use this option to disable color output')
2191 options, args = parser.parse_args(args)
2192
2193 author = RunGit(['config', 'user.email']).strip() or None
2194
2195 cl = Changelist()
2196
2197 if args:
2198 if len(args) > 1:
2199 parser.error('Unknown args')
2200 base_branch = args[0]
2201 else:
2202 # Default to diffing against the common ancestor of the upstream branch.
2203 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2204
2205 change = cl.GetChange(base_branch, None)
2206 return owners_finder.OwnersFinder(
2207 [f.LocalPath() for f in
2208 cl.GetChange(base_branch, None).AffectedFiles()],
2209 change.RepositoryRoot(), author,
2210 fopen=file, os_path=os.path, glob=glob.glob,
2211 disable_color=options.no_color).run()
2212
2213
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002214def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002215 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002216 CLANG_EXTS = ['.cc', '.cpp', '.h']
2217 parser.add_option('--full', action='store_true', default=False)
2218 opts, args = parser.parse_args(args)
2219 if args:
2220 parser.error('Unrecognized args: %s' % ' '.join(args))
2221
digit@chromium.org29e47272013-05-17 17:01:46 +00002222 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002223 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002224 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002225 # Only list the names of modified files.
2226 diff_cmd.append('--name-only')
2227 else:
2228 # Only generate context-less patches.
2229 diff_cmd.append('-U0')
2230
2231 # Grab the merge-base commit, i.e. the upstream commit of the current
2232 # branch when it was created or the last time it was rebased. This is
2233 # to cover the case where the user may have called "git fetch origin",
2234 # moving the origin branch to a newer commit, but hasn't rebased yet.
2235 upstream_commit = None
2236 cl = Changelist()
2237 upstream_branch = cl.GetUpstreamBranch()
2238 if upstream_branch:
2239 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2240 upstream_commit = upstream_commit.strip()
2241
2242 if not upstream_commit:
2243 DieWithError('Could not find base commit for this branch. '
2244 'Are you in detached state?')
2245
2246 diff_cmd.append(upstream_commit)
2247
2248 # Handle source file filtering.
2249 diff_cmd.append('--')
2250 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2251 diff_output = RunGit(diff_cmd)
2252
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002253 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2254
digit@chromium.org29e47272013-05-17 17:01:46 +00002255 if opts.full:
2256 # diff_output is a list of files to send to clang-format.
2257 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002258 if not files:
2259 print "Nothing to format."
2260 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002261 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2262 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002263 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002264 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002265 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2266 'clang-format-diff.py')
2267 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002268 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002269 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002270
2271 # Newer versions of clang-format-diff.py require an explicit -i flag
2272 # to apply the edits to files, otherwise it just displays a diff.
2273 # Probe the usage string to verify if this is needed.
2274 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2275 if '[-i]' in help_text:
2276 cmd.append('-i')
2277
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002278 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002279
2280 return 0
2281
2282
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002283class OptionParser(optparse.OptionParser):
2284 """Creates the option parse and add --verbose support."""
2285 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002286 optparse.OptionParser.__init__(
2287 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002288 self.add_option(
2289 '-v', '--verbose', action='count', default=0,
2290 help='Use 2 times for more debugging info')
2291
2292 def parse_args(self, args=None, values=None):
2293 options, args = optparse.OptionParser.parse_args(self, args, values)
2294 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2295 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2296 return options, args
2297
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002299def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002300 if sys.hexversion < 0x02060000:
2301 print >> sys.stderr, (
2302 '\nYour python version %s is unsupported, please upgrade.\n' %
2303 sys.version.split(' ', 1)[0])
2304 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002305
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002306 # Reload settings.
2307 global settings
2308 settings = Settings()
2309
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002310 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002311 dispatcher = subcommand.CommandDispatcher(__name__)
2312 try:
2313 return dispatcher.execute(OptionParser(), argv)
2314 except urllib2.HTTPError, e:
2315 if e.code != 500:
2316 raise
2317 DieWithError(
2318 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2319 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002320
2321
2322if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002323 # These affect sys.stdout so do it outside of main() to simplify mocks in
2324 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002325 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002326 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327 sys.exit(main(sys.argv[1:]))