blob: 7a9bff92d10e9ff14eb0b16983b0550aa00c939c [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'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000049GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
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):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 """Returns a tuple containg remote and remote ref,
477 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:
705 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000706 self.issue = None
707 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000709 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000710 if not self.GitSanityChecks(upstream_branch):
711 DieWithError('\nGit sanity check failure')
712
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000713 env = os.environ.copy()
714 # 'cat' is a magical git string that disables pagers on all platforms.
715 env['GIT_PAGER'] = 'cat'
716
717 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000718 if not root:
719 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000720 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000721
722 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000723 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000724 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000725 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000726 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000727 except subprocess2.CalledProcessError:
728 DieWithError(
729 ('\nFailed to diff against upstream branch %s!\n\n'
730 'This branch probably doesn\'t exist anymore. To reset the\n'
731 'tracking branch, please run\n'
732 ' git branch --set-upstream %s trunk\n'
733 'replacing trunk with origin/master or the relevant branch') %
734 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000735
maruel@chromium.org52424302012-08-29 15:14:30 +0000736 issue = self.GetIssue()
737 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000738 if issue:
739 description = self.GetDescription()
740 else:
741 # If the change was never uploaded, use the log messages of all commits
742 # up to the branch point, as git cl upload will prefill the description
743 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000744 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000745 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000746 '%s...' % (upstream_branch)],
747 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000748
749 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000750 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000751 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000752 name,
753 description,
754 absroot,
755 files,
756 issue,
757 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000758 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000759
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000760 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000761 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000762
763 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000764 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000765 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000766 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000767 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000768 except presubmit_support.PresubmitFailure, e:
769 DieWithError(
770 ('%s\nMaybe your depot_tools is out of date?\n'
771 'If all fails, contact maruel@') % e)
772
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000773 def UpdateDescription(self, description):
774 self.description = description
775 return self.RpcServer().update_description(
776 self.GetIssue(), self.description)
777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000779 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000780 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000782 def SetFlag(self, flag, value):
783 """Patchset must match."""
784 if not self.GetPatchset():
785 DieWithError('The patchset needs to match. Send another patchset.')
786 try:
787 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000788 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000789 except urllib2.HTTPError, e:
790 if e.code == 404:
791 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
792 if e.code == 403:
793 DieWithError(
794 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
795 'match?') % (self.GetIssue(), self.GetPatchset()))
796 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000798 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799 """Returns an upload.RpcServer() to access this review's rietveld instance.
800 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000801 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000802 self._rpc_server = rietveld.CachingRietveld(
803 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000804 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805
806 def _IssueSetting(self):
807 """Return the git setting that stores this change's issue."""
808 return 'branch.%s.rietveldissue' % self.GetBranch()
809
810 def _PatchsetSetting(self):
811 """Return the git setting that stores this change's most recent patchset."""
812 return 'branch.%s.rietveldpatchset' % self.GetBranch()
813
814 def _RietveldServer(self):
815 """Returns the git setting that stores this change's rietveld server."""
816 return 'branch.%s.rietveldserver' % self.GetBranch()
817
818
819def GetCodereviewSettingsInteractively():
820 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000821 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 server = settings.GetDefaultServerUrl(error_ok=True)
823 prompt = 'Rietveld server (host[:port])'
824 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000825 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 if not server and not newserver:
827 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000828 if newserver:
829 newserver = gclient_utils.UpgradeToHttps(newserver)
830 if newserver != server:
831 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000833 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 prompt = caption
835 if initial:
836 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000837 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 if new_val == 'x':
839 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000840 elif new_val:
841 if is_url:
842 new_val = gclient_utils.UpgradeToHttps(new_val)
843 if new_val != initial:
844 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000846 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000847 SetProperty(settings.GetDefaultPrivateFlag(),
848 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000850 'tree-status-url', False)
851 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
853 # TODO: configure a default branch to diff against, rather than this
854 # svn-based hackery.
855
856
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000857class ChangeDescription(object):
858 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000859 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000860 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000861
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000862 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000863 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000864
agable@chromium.org42c20792013-09-12 17:34:49 +0000865 @property # www.logilab.org/ticket/89786
866 def description(self): # pylint: disable=E0202
867 return '\n'.join(self._description_lines)
868
869 def set_description(self, desc):
870 if isinstance(desc, basestring):
871 lines = desc.splitlines()
872 else:
873 lines = [line.rstrip() for line in desc]
874 while lines and not lines[0]:
875 lines.pop(0)
876 while lines and not lines[-1]:
877 lines.pop(-1)
878 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000879
880 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000881 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882 assert isinstance(reviewers, list), reviewers
883 if not reviewers:
884 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000885 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000886
agable@chromium.org42c20792013-09-12 17:34:49 +0000887 # Get the set of R= and TBR= lines and remove them from the desciption.
888 regexp = re.compile(self.R_LINE)
889 matches = [regexp.match(line) for line in self._description_lines]
890 new_desc = [l for i, l in enumerate(self._description_lines)
891 if not matches[i]]
892 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000893
agable@chromium.org42c20792013-09-12 17:34:49 +0000894 # Construct new unified R= and TBR= lines.
895 r_names = []
896 tbr_names = []
897 for match in matches:
898 if not match:
899 continue
900 people = cleanup_list([match.group(2).strip()])
901 if match.group(1) == 'TBR':
902 tbr_names.extend(people)
903 else:
904 r_names.extend(people)
905 for name in r_names:
906 if name not in reviewers:
907 reviewers.append(name)
908 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
909 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
910
911 # Put the new lines in the description where the old first R= line was.
912 line_loc = next((i for i, match in enumerate(matches) if match), -1)
913 if 0 <= line_loc < len(self._description_lines):
914 if new_tbr_line:
915 self._description_lines.insert(line_loc, new_tbr_line)
916 if new_r_line:
917 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000918 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000919 if new_r_line:
920 self.append_footer(new_r_line)
921 if new_tbr_line:
922 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000923
924 def prompt(self):
925 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000926 self.set_description([
927 '# Enter a description of the change.',
928 '# This will be displayed on the codereview site.',
929 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000930 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000931 '--------------------',
932 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000933
agable@chromium.org42c20792013-09-12 17:34:49 +0000934 regexp = re.compile(self.BUG_LINE)
935 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000936 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000937 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000938 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000939 if not content:
940 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000941 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000942
943 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000944 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
945 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000946 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000948
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000949 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000950 if self._description_lines:
951 # Add an empty line if either the last line or the new line isn't a tag.
952 last_line = self._description_lines[-1]
953 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
954 not presubmit_support.Change.TAG_LINE_RE.match(line)):
955 self._description_lines.append('')
956 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000957
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000958 def get_reviewers(self):
959 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000960 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
961 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000962 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000963
964
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000965def get_approving_reviewers(props):
966 """Retrieves the reviewers that approved a CL from the issue properties with
967 messages.
968
969 Note that the list may contain reviewers that are not committer, thus are not
970 considered by the CQ.
971 """
972 return sorted(
973 set(
974 message['sender']
975 for message in props['messages']
976 if message['approval'] and message['sender'] in props['reviewers']
977 )
978 )
979
980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981def FindCodereviewSettingsFile(filename='codereview.settings'):
982 """Finds the given file starting in the cwd and going up.
983
984 Only looks up to the top of the repository unless an
985 'inherit-review-settings-ok' file exists in the root of the repository.
986 """
987 inherit_ok_file = 'inherit-review-settings-ok'
988 cwd = os.getcwd()
989 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
990 if os.path.isfile(os.path.join(root, inherit_ok_file)):
991 root = '/'
992 while True:
993 if filename in os.listdir(cwd):
994 if os.path.isfile(os.path.join(cwd, filename)):
995 return open(os.path.join(cwd, filename))
996 if cwd == root:
997 break
998 cwd = os.path.dirname(cwd)
999
1000
1001def LoadCodereviewSettingsFromFile(fileobj):
1002 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001003 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 def SetProperty(name, setting, unset_error_ok=False):
1006 fullname = 'rietveld.' + name
1007 if setting in keyvals:
1008 RunGit(['config', fullname, keyvals[setting]])
1009 else:
1010 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1011
1012 SetProperty('server', 'CODE_REVIEW_SERVER')
1013 # Only server setting is required. Other settings can be absent.
1014 # In that case, we ignore errors raised during option deletion attempt.
1015 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001016 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1018 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1019
ukai@chromium.orge8077812012-02-03 03:41:46 +00001020 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
1021 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
1022 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001023
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1025 #should be of the form
1026 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1027 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1028 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1029 keyvals['ORIGIN_URL_CONFIG']])
1030
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001032def urlretrieve(source, destination):
1033 """urllib is broken for SSL connections via a proxy therefore we
1034 can't use urllib.urlretrieve()."""
1035 with open(destination, 'w') as f:
1036 f.write(urllib2.urlopen(source).read())
1037
1038
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001039def DownloadHooks(force):
1040 """downloads hooks
1041
1042 Args:
1043 force: True to update hooks. False to install hooks if not present.
1044 """
1045 if not settings.GetIsGerrit():
1046 return
1047 server_url = settings.GetDefaultServerUrl()
1048 src = '%s/tools/hooks/commit-msg' % server_url
1049 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1050 if not os.access(dst, os.X_OK):
1051 if os.path.exists(dst):
1052 if not force:
1053 return
1054 os.remove(dst)
1055 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001056 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001057 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1058 except Exception:
1059 if os.path.exists(dst):
1060 os.remove(dst)
1061 DieWithError('\nFailed to download hooks from %s' % src)
1062
1063
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001064@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001066 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001068 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069 if len(args) == 0:
1070 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001071 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 return 0
1073
1074 url = args[0]
1075 if not url.endswith('codereview.settings'):
1076 url = os.path.join(url, 'codereview.settings')
1077
1078 # Load code review settings and download hooks (if available).
1079 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001080 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 return 0
1082
1083
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001084def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001085 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001086 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1087 branch = ShortBranchName(branchref)
1088 _, args = parser.parse_args(args)
1089 if not args:
1090 print("Current base-url:")
1091 return RunGit(['config', 'branch.%s.base-url' % branch],
1092 error_ok=False).strip()
1093 else:
1094 print("Setting base-url to %s" % args[0])
1095 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1096 error_ok=False).strip()
1097
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001100 """Show status of changelists.
1101
1102 Colors are used to tell the state of the CL unless --fast is used:
1103 - Green LGTM'ed
1104 - Blue waiting for review
1105 - Yellow waiting for you to reply to review
1106 - Red not sent for review or broken
1107 - Cyan was committed, branch can be deleted
1108
1109 Also see 'git cl comments'.
1110 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 parser.add_option('--field',
1112 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001113 parser.add_option('-f', '--fast', action='store_true',
1114 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001116 if args:
1117 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001120 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121 if options.field.startswith('desc'):
1122 print cl.GetDescription()
1123 elif options.field == 'id':
1124 issueid = cl.GetIssue()
1125 if issueid:
1126 print issueid
1127 elif options.field == 'patch':
1128 patchset = cl.GetPatchset()
1129 if patchset:
1130 print patchset
1131 elif options.field == 'url':
1132 url = cl.GetIssueURL()
1133 if url:
1134 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001135 return 0
1136
1137 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1138 if not branches:
1139 print('No local branch found.')
1140 return 0
1141
1142 changes = (Changelist(branchref=b) for b in branches.splitlines())
1143 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1144 alignment = max(5, max(len(b) for b in branches))
1145 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001146 # Adhoc thread pool to request data concurrently.
1147 output = Queue.Queue()
1148
1149 # Silence upload.py otherwise it becomes unweldly.
1150 upload.verbosity = 0
1151
1152 if not options.fast:
1153 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001154 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001155 c = Changelist(branchref=b)
1156 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001157 props = {}
1158 r = None
1159 if i:
1160 try:
1161 props = c.GetIssueProperties()
1162 r = c.GetApprovingReviewers() if i else None
1163 except urllib2.HTTPError:
1164 # The issue probably doesn't exist anymore.
1165 i += ' (broken)'
1166
1167 msgs = props.get('messages') or []
1168
1169 if not i:
1170 color = Fore.WHITE
1171 elif props.get('closed'):
1172 # Issue is closed.
1173 color = Fore.CYAN
1174 elif r:
1175 # Was LGTM'ed.
1176 color = Fore.GREEN
1177 elif not msgs:
1178 # No message was sent.
1179 color = Fore.RED
1180 elif msgs[-1]['sender'] != props.get('owner_email'):
1181 color = Fore.YELLOW
1182 else:
1183 color = Fore.BLUE
1184 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001185
1186 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1187 for t in threads:
1188 t.daemon = True
1189 t.start()
1190 else:
1191 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1192 for b in branches:
1193 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001194 url = c.GetIssueURL()
1195 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001196
1197 tmp = {}
1198 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001199 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001200 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001201 b, i, color = output.get()
1202 tmp[b] = (i, color)
1203 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001204 reset = Fore.RESET
1205 if not sys.stdout.isatty():
1206 color = ''
1207 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001209 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001210
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001211 cl = Changelist()
1212 print
1213 print 'Current branch:',
1214 if not cl.GetIssue():
1215 print 'no issue assigned.'
1216 return 0
1217 print cl.GetBranch()
1218 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1219 print 'Issue description:'
1220 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 return 0
1222
1223
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001224def colorize_CMDstatus_doc():
1225 """To be called once in main() to add colors to git cl status help."""
1226 colors = [i for i in dir(Fore) if i[0].isupper()]
1227
1228 def colorize_line(line):
1229 for color in colors:
1230 if color in line.upper():
1231 # Extract whitespaces first and the leading '-'.
1232 indent = len(line) - len(line.lstrip(' ')) + 1
1233 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1234 return line
1235
1236 lines = CMDstatus.__doc__.splitlines()
1237 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1238
1239
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001240@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001242 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243
1244 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001245 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001246 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247
1248 cl = Changelist()
1249 if len(args) > 0:
1250 try:
1251 issue = int(args[0])
1252 except ValueError:
1253 DieWithError('Pass a number to set the issue or none to list it.\n'
1254 'Maybe you want to run git cl status?')
1255 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001256 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 return 0
1258
1259
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001260def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001261 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001262 (_, args) = parser.parse_args(args)
1263 if args:
1264 parser.error('Unsupported argument: %s' % args)
1265
1266 cl = Changelist()
1267 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001268 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001269 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001270 if message['disapproval']:
1271 color = Fore.RED
1272 elif message['approval']:
1273 color = Fore.GREEN
1274 elif message['sender'] == data['owner_email']:
1275 color = Fore.MAGENTA
1276 else:
1277 color = Fore.BLUE
1278 print '\n%s%s %s%s' % (
1279 color, message['date'].split('.', 1)[0], message['sender'],
1280 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001281 if message['text'].strip():
1282 print '\n'.join(' ' + l for l in message['text'].splitlines())
1283 return 0
1284
1285
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001286def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001287 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001288 cl = Changelist()
1289 if not cl.GetIssue():
1290 DieWithError('This branch has no associated changelist.')
1291 description = ChangeDescription(cl.GetDescription())
1292 description.prompt()
1293 cl.UpdateDescription(description.description)
1294 return 0
1295
1296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297def CreateDescriptionFromLog(args):
1298 """Pulls out the commit log to use as a base for the CL description."""
1299 log_args = []
1300 if len(args) == 1 and not args[0].endswith('.'):
1301 log_args = [args[0] + '..']
1302 elif len(args) == 1 and args[0].endswith('...'):
1303 log_args = [args[0][:-1]]
1304 elif len(args) == 2:
1305 log_args = [args[0] + '..' + args[1]]
1306 else:
1307 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001308 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309
1310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001312 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001313 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001315 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001316 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 (options, args) = parser.parse_args(args)
1318
ukai@chromium.org259e4682012-10-25 07:36:33 +00001319 if not options.force and is_dirty_git_tree('presubmit'):
1320 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 return 1
1322
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001323 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324 if args:
1325 base_branch = args[0]
1326 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 # Default to diffing against the common ancestor of the upstream branch.
1328 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001330 cl.RunHook(
1331 committing=not options.upload,
1332 may_prompt=False,
1333 verbose=options.verbose,
1334 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001335 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336
1337
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001338def AddChangeIdToCommitMessage(options, args):
1339 """Re-commits using the current message, assumes the commit hook is in
1340 place.
1341 """
1342 log_desc = options.message or CreateDescriptionFromLog(args)
1343 git_command = ['commit', '--amend', '-m', log_desc]
1344 RunGit(git_command)
1345 new_log_desc = CreateDescriptionFromLog(args)
1346 if CHANGE_ID in new_log_desc:
1347 print 'git-cl: Added Change-Id to commit message.'
1348 else:
1349 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1350
1351
ukai@chromium.orge8077812012-02-03 03:41:46 +00001352def GerritUpload(options, args, cl):
1353 """upload the current branch to gerrit."""
1354 # We assume the remote called "origin" is the one we want.
1355 # It is probably not worthwhile to support different workflows.
1356 remote = 'origin'
1357 branch = 'master'
1358 if options.target_branch:
1359 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001361 change_desc = ChangeDescription(
1362 options.message or CreateDescriptionFromLog(args))
1363 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001364 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001366 if CHANGE_ID not in change_desc.description:
1367 AddChangeIdToCommitMessage(options, args)
1368 if options.reviewers:
1369 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
ukai@chromium.orge8077812012-02-03 03:41:46 +00001371 receive_options = []
1372 cc = cl.GetCCList().split(',')
1373 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001374 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001375 cc = filter(None, cc)
1376 if cc:
1377 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001378 if change_desc.get_reviewers():
1379 receive_options.extend(
1380 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
ukai@chromium.orge8077812012-02-03 03:41:46 +00001382 git_command = ['push']
1383 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001384 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385 ' '.join(receive_options))
1386 git_command += [remote, 'HEAD:refs/for/' + branch]
1387 RunGit(git_command)
1388 # TODO(ukai): parse Change-Id: and set issue number?
1389 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001391
ukai@chromium.orge8077812012-02-03 03:41:46 +00001392def RietveldUpload(options, args, cl):
1393 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1395 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 if options.emulate_svn_auto_props:
1397 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398
1399 change_desc = None
1400
1401 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001402 if options.title:
1403 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001404 if options.message:
1405 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001406 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 print ("This branch is associated with issue %s. "
1408 "Adding patch to that issue." % cl.GetIssue())
1409 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001410 if options.title:
1411 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001412 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001413 change_desc = ChangeDescription(message)
1414 if options.reviewers:
1415 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001416 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001417 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001418
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001419 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 print "Description is empty; aborting."
1421 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001422
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001423 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001424 if change_desc.get_reviewers():
1425 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001426 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001427 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001428 DieWithError("Must specify reviewers to send email.")
1429 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001430
1431 # We check this before applying rietveld.private assuming that in
1432 # rietveld.cc only addresses which we can send private CLs to are listed
1433 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1434 # --private is specified explicitly on the command line.
1435 if options.private:
1436 logging.warn('rietveld.cc is ignored since private flag is specified. '
1437 'You need to review and add them manually if necessary.')
1438 cc = cl.GetCCListWithoutDefault()
1439 else:
1440 cc = cl.GetCCList()
1441 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001442 if cc:
1443 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001445 if options.private or settings.GetDefaultPrivateFlag() == "True":
1446 upload_args.append('--private')
1447
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001448 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001449 if not options.find_copies:
1450 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452 # Include the upstream repo's URL in the change -- this is useful for
1453 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001454 remote_url = cl.GetGitBaseUrlFromConfig()
1455 if not remote_url:
1456 if settings.GetIsGitSvn():
1457 # URL is dependent on the current directory.
1458 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1459 if data:
1460 keys = dict(line.split(': ', 1) for line in data.splitlines()
1461 if ': ' in line)
1462 remote_url = keys.get('URL', None)
1463 else:
1464 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1465 remote_url = (cl.GetRemoteUrl() + '@'
1466 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 if remote_url:
1468 upload_args.extend(['--base_url', remote_url])
1469
1470 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001471 upload_args = ['upload'] + upload_args + args
1472 logging.info('upload.RealMain(%s)', upload_args)
1473 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001474 issue = int(issue)
1475 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001476 except KeyboardInterrupt:
1477 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 except:
1479 # If we got an exception after the user typed a description for their
1480 # change, back up the description before re-raising.
1481 if change_desc:
1482 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1483 print '\nGot exception while uploading -- saving description to %s\n' \
1484 % backup_path
1485 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001486 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 backup_file.close()
1488 raise
1489
1490 if not cl.GetIssue():
1491 cl.SetIssue(issue)
1492 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001493
1494 if options.use_commit_queue:
1495 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496 return 0
1497
1498
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001499def cleanup_list(l):
1500 """Fixes a list so that comma separated items are put as individual items.
1501
1502 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1503 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1504 """
1505 items = sum((i.split(',') for i in l), [])
1506 stripped_items = (i.strip() for i in items)
1507 return sorted(filter(None, stripped_items))
1508
1509
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001510@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001511def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001512 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001513 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1514 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001515 parser.add_option('--bypass-watchlists', action='store_true',
1516 dest='bypass_watchlists',
1517 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001518 parser.add_option('-f', action='store_true', dest='force',
1519 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001520 parser.add_option('-m', dest='message', help='message for patchset')
1521 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001522 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001523 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001524 help='reviewer email addresses')
1525 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001526 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001527 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001528 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001529 help='send email to reviewer immediately')
1530 parser.add_option("--emulate_svn_auto_props", action="store_true",
1531 dest="emulate_svn_auto_props",
1532 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001533 parser.add_option('-c', '--use-commit-queue', action='store_true',
1534 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001535 parser.add_option('--private', action='store_true',
1536 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001537 parser.add_option('--target_branch',
1538 help='When uploading to gerrit, remote branch to '
1539 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001540 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541 (options, args) = parser.parse_args(args)
1542
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001543 if options.target_branch and not settings.GetIsGerrit():
1544 parser.error('Use --target_branch for non gerrit repository.')
1545
ukai@chromium.org259e4682012-10-25 07:36:33 +00001546 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547 return 1
1548
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001549 options.reviewers = cleanup_list(options.reviewers)
1550 options.cc = cleanup_list(options.cc)
1551
ukai@chromium.orge8077812012-02-03 03:41:46 +00001552 cl = Changelist()
1553 if args:
1554 # TODO(ukai): is it ok for gerrit case?
1555 base_branch = args[0]
1556 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001557 # Default to diffing against common ancestor of upstream branch
1558 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001559 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001560
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001561 # Apply watchlists on upload.
1562 change = cl.GetChange(base_branch, None)
1563 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1564 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001565 if not options.bypass_watchlists:
1566 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001567
ukai@chromium.orge8077812012-02-03 03:41:46 +00001568 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001569 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001570 may_prompt=not options.force,
1571 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001572 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573 if not hook_results.should_continue():
1574 return 1
1575 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001576 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001577
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001578 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001579 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001580 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001581 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001582 print ('The last upload made from this repository was patchset #%d but '
1583 'the most recent patchset on the server is #%d.'
1584 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001585 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1586 'from another machine or branch the patch you\'re uploading now '
1587 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001588 ask_for_data('About to upload; enter to confirm.')
1589
iannucci@chromium.org79540052012-10-19 23:15:26 +00001590 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001591 if settings.GetIsGerrit():
1592 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001593 ret = RietveldUpload(options, args, cl)
1594 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001595 git_set_branch_value('last-upload-hash',
1596 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001597
1598 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001599
1600
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001601def IsSubmoduleMergeCommit(ref):
1602 # When submodules are added to the repo, we expect there to be a single
1603 # non-git-svn merge commit at remote HEAD with a signature comment.
1604 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001605 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001606 return RunGit(cmd) != ''
1607
1608
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609def SendUpstream(parser, args, cmd):
1610 """Common code for CmdPush and CmdDCommit
1611
1612 Squashed commit into a single.
1613 Updates changelog with metadata (e.g. pointer to review).
1614 Pushes/dcommits the code upstream.
1615 Updates review and closes.
1616 """
1617 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1618 help='bypass upload presubmit hook')
1619 parser.add_option('-m', dest='message',
1620 help="override review description")
1621 parser.add_option('-f', action='store_true', dest='force',
1622 help="force yes to questions (don't prompt)")
1623 parser.add_option('-c', dest='contributor',
1624 help="external contributor for patch (appended to " +
1625 "description and used as author for git). Should be " +
1626 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001627 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628 (options, args) = parser.parse_args(args)
1629 cl = Changelist()
1630
1631 if not args or cmd == 'push':
1632 # Default to merging against our best guess of the upstream branch.
1633 args = [cl.GetUpstreamBranch()]
1634
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001635 if options.contributor:
1636 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1637 print "Please provide contibutor as 'First Last <email@example.com>'"
1638 return 1
1639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001641 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642
ukai@chromium.org259e4682012-10-25 07:36:33 +00001643 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001644 return 1
1645
1646 # This rev-list syntax means "show all commits not in my branch that
1647 # are in base_branch".
1648 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1649 base_branch]).splitlines()
1650 if upstream_commits:
1651 print ('Base branch "%s" has %d commits '
1652 'not in this branch.' % (base_branch, len(upstream_commits)))
1653 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1654 return 1
1655
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001656 # This is the revision `svn dcommit` will commit on top of.
1657 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1658 '--pretty=format:%H'])
1659
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001660 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001661 # If the base_head is a submodule merge commit, the first parent of the
1662 # base_head should be a git-svn commit, which is what we're interested in.
1663 base_svn_head = base_branch
1664 if base_has_submodules:
1665 base_svn_head += '^1'
1666
1667 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001668 if extra_commits:
1669 print ('This branch has %d additional commits not upstreamed yet.'
1670 % len(extra_commits.splitlines()))
1671 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1672 'before attempting to %s.' % (base_branch, cmd))
1673 return 1
1674
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001675 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001676 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001677 author = None
1678 if options.contributor:
1679 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001680 hook_results = cl.RunHook(
1681 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001682 may_prompt=not options.force,
1683 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001684 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001685 if not hook_results.should_continue():
1686 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001687
1688 if cmd == 'dcommit':
1689 # Check the tree status if the tree status URL is set.
1690 status = GetTreeStatus()
1691 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001692 print('The tree is closed. Please wait for it to reopen. Use '
1693 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001694 return 1
1695 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001696 print('Unable to determine tree status. Please verify manually and '
1697 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001698 else:
1699 breakpad.SendStack(
1700 'GitClHooksBypassedCommit',
1701 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001702 (cl.GetRietveldServer(), cl.GetIssue()),
1703 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001705 change_desc = ChangeDescription(options.message)
1706 if not change_desc.description and cl.GetIssue():
1707 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001709 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001710 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001711 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001712 else:
1713 print 'No description set.'
1714 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1715 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001717 # Keep a separate copy for the commit message, because the commit message
1718 # contains the link to the Rietveld issue, while the Rietveld message contains
1719 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001720 # Keep a separate copy for the commit message.
1721 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001722 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001723
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001724 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001725 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001726 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001727 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001728 commit_desc.append_footer('Patch from %s.' % options.contributor)
1729
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001730 print('Description:')
1731 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732
1733 branches = [base_branch, cl.GetBranchRef()]
1734 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001735 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001736 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001738 # We want to squash all this branch's commits into one commit with the proper
1739 # description. We do this by doing a "reset --soft" to the base branch (which
1740 # keeps the working copy the same), then dcommitting that. If origin/master
1741 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1742 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001744 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1745 # Delete the branches if they exist.
1746 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1747 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1748 result = RunGitWithCode(showref_cmd)
1749 if result[0] == 0:
1750 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751
1752 # We might be in a directory that's present in this branch but not in the
1753 # trunk. Move up to the top of the tree so that git commands that expect a
1754 # valid CWD won't fail after we check out the merge branch.
1755 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1756 if rel_base_path:
1757 os.chdir(rel_base_path)
1758
1759 # Stuff our change into the merge branch.
1760 # We wrap in a try...finally block so if anything goes wrong,
1761 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001762 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001764 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1765 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001767 RunGit(
1768 [
1769 'commit', '--author', options.contributor,
1770 '-m', commit_desc.description,
1771 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001773 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001774 if base_has_submodules:
1775 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1776 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1777 RunGit(['checkout', CHERRY_PICK_BRANCH])
1778 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 if cmd == 'push':
1780 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001781 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782 retcode, output = RunGitWithCode(
1783 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1784 logging.debug(output)
1785 else:
1786 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001787 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001788 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001789 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790 finally:
1791 # And then swap back to the original branch and clean up.
1792 RunGit(['checkout', '-q', cl.GetBranch()])
1793 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001794 if base_has_submodules:
1795 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796
1797 if cl.GetIssue():
1798 if cmd == 'dcommit' and 'Committed r' in output:
1799 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1800 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001801 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1802 for l in output.splitlines(False))
1803 match = filter(None, match)
1804 if len(match) != 1:
1805 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1806 output)
1807 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808 else:
1809 return 1
1810 viewvc_url = settings.GetViewVCUrl()
1811 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001812 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001813 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001814 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 print ('Closing issue '
1816 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001817 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001819 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001820 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001821 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001822 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1823 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001824 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001825
1826 if retcode == 0:
1827 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1828 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001829 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831 return 0
1832
1833
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001834@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001836 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001837 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001838 message = """This doesn't appear to be an SVN repository.
1839If your project has a git mirror with an upstream SVN master, you probably need
1840to run 'git svn init', see your project's git mirror documentation.
1841If your project has a true writeable upstream repository, you probably want
1842to run 'git cl push' instead.
1843Choose wisely, if you get this wrong, your commit might appear to succeed but
1844will instead be silently ignored."""
1845 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001846 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847 return SendUpstream(parser, args, 'dcommit')
1848
1849
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001850@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001852 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853 if settings.GetIsGitSvn():
1854 print('This appears to be an SVN repository.')
1855 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001856 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001857 return SendUpstream(parser, args, 'push')
1858
1859
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001860@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001862 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 parser.add_option('-b', dest='newbranch',
1864 help='create a new branch off trunk for the patch')
1865 parser.add_option('-f', action='store_true', dest='force',
1866 help='with -b, clobber any existing branch')
1867 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001868 help='failed patches spew .rej files rather than '
1869 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001870 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1871 help="don't commit after patch applies")
1872 (options, args) = parser.parse_args(args)
1873 if len(args) != 1:
1874 parser.print_help()
1875 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001876 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001877
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001878 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001879 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001880
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001881 if options.newbranch:
1882 if options.force:
1883 RunGit(['branch', '-D', options.newbranch],
1884 stderr=subprocess2.PIPE, error_ok=True)
1885 RunGit(['checkout', '-b', options.newbranch,
1886 Changelist().GetUpstreamBranch()])
1887
1888 return PatchIssue(issue_arg, options.reject, options.nocommit)
1889
1890
1891def PatchIssue(issue_arg, reject, nocommit):
1892 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001894 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001895 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001896 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001897 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001899 # Assume it's a URL to the patch. Default to https.
1900 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001901 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001902 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 DieWithError('Must pass an issue ID or full URL for '
1904 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001905 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001906 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001907 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 # Switch up to the top-level directory, if necessary, in preparation for
1910 # applying the patch.
1911 top = RunGit(['rev-parse', '--show-cdup']).strip()
1912 if top:
1913 os.chdir(top)
1914
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915 # Git patches have a/ at the beginning of source paths. We strip that out
1916 # with a sed script rather than the -p flag to patch so we can feed either
1917 # Git or svn-style patches into the same apply command.
1918 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001919 try:
1920 patch_data = subprocess2.check_output(
1921 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1922 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923 DieWithError('Git patch mungling failed.')
1924 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001925 env = os.environ.copy()
1926 # 'cat' is a magical git string that disables pagers on all platforms.
1927 env['GIT_PAGER'] = 'cat'
1928
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929 # We use "git apply" to apply the patch instead of "patch" so that we can
1930 # pick up file adds.
1931 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001932 cmd = ['git', 'apply', '--index', '-p0']
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001933 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001935 elif IsGitVersionAtLeast('1.7.12'):
1936 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001937 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001938 subprocess2.check_call(cmd, env=env,
1939 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001940 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941 DieWithError('Failed to apply the patch')
1942
1943 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001944 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1946 cl = Changelist()
1947 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001948 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001949 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950 else:
1951 print "Patch applied to index."
1952 return 0
1953
1954
1955def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001956 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001957 # Provide a wrapper for git svn rebase to help avoid accidental
1958 # git svn dcommit.
1959 # It's the only command that doesn't use parser at all since we just defer
1960 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001961 env = os.environ.copy()
1962 # 'cat' is a magical git string that disables pagers on all platforms.
1963 env['GIT_PAGER'] = 'cat'
1964
1965 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966
1967
1968def GetTreeStatus():
1969 """Fetches the tree status and returns either 'open', 'closed',
1970 'unknown' or 'unset'."""
1971 url = settings.GetTreeStatusUrl(error_ok=True)
1972 if url:
1973 status = urllib2.urlopen(url).read().lower()
1974 if status.find('closed') != -1 or status == '0':
1975 return 'closed'
1976 elif status.find('open') != -1 or status == '1':
1977 return 'open'
1978 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 return 'unset'
1980
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001981
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001982def GetTreeStatusReason():
1983 """Fetches the tree status from a json url and returns the message
1984 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001985 url = settings.GetTreeStatusUrl()
1986 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987 connection = urllib2.urlopen(json_url)
1988 status = json.loads(connection.read())
1989 connection.close()
1990 return status['message']
1991
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001992
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001994 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001995 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001996 status = GetTreeStatus()
1997 if 'unset' == status:
1998 print 'You must configure your tree status URL by running "git cl config".'
1999 return 2
2000
2001 print "The tree is %s" % status
2002 print
2003 print GetTreeStatusReason()
2004 if status != 'open':
2005 return 1
2006 return 0
2007
2008
maruel@chromium.org15192402012-09-06 12:38:29 +00002009def CMDtry(parser, args):
2010 """Triggers a try job through Rietveld."""
2011 group = optparse.OptionGroup(parser, "Try job options")
2012 group.add_option(
2013 "-b", "--bot", action="append",
2014 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2015 "times to specify multiple builders. ex: "
2016 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2017 "the try server waterfall for the builders name and the tests "
2018 "available. Can also be used to specify gtest_filter, e.g. "
2019 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2020 group.add_option(
2021 "-r", "--revision",
2022 help="Revision to use for the try job; default: the "
2023 "revision will be determined by the try server; see "
2024 "its waterfall for more info")
2025 group.add_option(
2026 "-c", "--clobber", action="store_true", default=False,
2027 help="Force a clobber before building; e.g. don't do an "
2028 "incremental build")
2029 group.add_option(
2030 "--project",
2031 help="Override which project to use. Projects are defined "
2032 "server-side to define what default bot set to use")
2033 group.add_option(
2034 "-t", "--testfilter", action="append", default=[],
2035 help=("Apply a testfilter to all the selected builders. Unless the "
2036 "builders configurations are similar, use multiple "
2037 "--bot <builder>:<test> arguments."))
2038 group.add_option(
2039 "-n", "--name", help="Try job name; default to current branch name")
2040 parser.add_option_group(group)
2041 options, args = parser.parse_args(args)
2042
2043 if args:
2044 parser.error('Unknown arguments: %s' % args)
2045
2046 cl = Changelist()
2047 if not cl.GetIssue():
2048 parser.error('Need to upload first')
2049
2050 if not options.name:
2051 options.name = cl.GetBranch()
2052
2053 # Process --bot and --testfilter.
2054 if not options.bot:
2055 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002056 change = cl.GetChange(
2057 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2058 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002059 options.bot = presubmit_support.DoGetTrySlaves(
2060 change,
2061 change.LocalPaths(),
2062 settings.GetRoot(),
2063 None,
2064 None,
2065 options.verbose,
2066 sys.stdout)
2067 if not options.bot:
2068 parser.error('No default try builder to try, use --bot')
2069
2070 builders_and_tests = {}
2071 for bot in options.bot:
2072 if ':' in bot:
2073 builder, tests = bot.split(':', 1)
2074 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2075 elif ',' in bot:
2076 parser.error('Specify one bot per --bot flag')
2077 else:
2078 builders_and_tests.setdefault(bot, []).append('defaulttests')
2079
2080 if options.testfilter:
2081 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2082 builders_and_tests = dict(
2083 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2084 if t != ['compile'])
2085
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002086 if any('triggered' in b for b in builders_and_tests):
2087 print >> sys.stderr, (
2088 'ERROR You are trying to send a job to a triggered bot. This type of'
2089 ' bot requires an\ninitial job from a parent (usually a builder). '
2090 'Instead send your job to the parent.\n'
2091 'Bot list: %s' % builders_and_tests)
2092 return 1
2093
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002094 patchset = cl.GetMostRecentPatchset()
2095 if patchset and patchset != cl.GetPatchset():
2096 print(
2097 '\nWARNING Mismatch between local config and server. Did a previous '
2098 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2099 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002100
2101 cl.RpcServer().trigger_try_jobs(
2102 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2103 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002104 print('Tried jobs on:')
2105 length = max(len(builder) for builder in builders_and_tests)
2106 for builder in sorted(builders_and_tests):
2107 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002108 return 0
2109
2110
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002111@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002112def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002113 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002114 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002115 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002116 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002117 return 0
2118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002120 if args:
2121 # One arg means set upstream branch.
2122 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2123 cl = Changelist()
2124 print "Upstream branch set to " + cl.GetUpstreamBranch()
2125 else:
2126 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 return 0
2128
2129
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002130def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002131 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002132 _, args = parser.parse_args(args)
2133 if args:
2134 parser.error('Unrecognized args: %s' % ' '.join(args))
2135 cl = Changelist()
2136 cl.SetFlag('commit', '1')
2137 return 0
2138
2139
groby@chromium.org411034a2013-02-26 15:12:01 +00002140def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002141 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002142 _, args = parser.parse_args(args)
2143 if args:
2144 parser.error('Unrecognized args: %s' % ' '.join(args))
2145 cl = Changelist()
2146 # Ensure there actually is an issue to close.
2147 cl.GetDescription()
2148 cl.CloseIssue()
2149 return 0
2150
2151
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002152def CMDdiff(parser, args):
2153 """shows differences between local tree and last upload."""
2154 cl = Changelist()
2155 branch = cl.GetBranch()
2156 TMP_BRANCH = 'git-cl-diff'
2157 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2158
2159 # Create a new branch based on the merge-base
2160 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2161 try:
2162 # Patch in the latest changes from rietveld.
2163 rtn = PatchIssue(cl.GetIssue(), False, False)
2164 if rtn != 0:
2165 return rtn
2166
2167 # Switch back to starting brand and diff against the temporary
2168 # branch containing the latest rietveld patch.
2169 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2170 finally:
2171 RunGit(['checkout', '-q', branch])
2172 RunGit(['branch', '-D', TMP_BRANCH])
2173
2174 return 0
2175
2176
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002177def CMDowners(parser, args):
2178 """interactively find the owners for reviewing"""
2179 parser.add_option(
2180 '--no-color',
2181 action='store_true',
2182 help='Use this option to disable color output')
2183 options, args = parser.parse_args(args)
2184
2185 author = RunGit(['config', 'user.email']).strip() or None
2186
2187 cl = Changelist()
2188
2189 if args:
2190 if len(args) > 1:
2191 parser.error('Unknown args')
2192 base_branch = args[0]
2193 else:
2194 # Default to diffing against the common ancestor of the upstream branch.
2195 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2196
2197 change = cl.GetChange(base_branch, None)
2198 return owners_finder.OwnersFinder(
2199 [f.LocalPath() for f in
2200 cl.GetChange(base_branch, None).AffectedFiles()],
2201 change.RepositoryRoot(), author,
2202 fopen=file, os_path=os.path, glob=glob.glob,
2203 disable_color=options.no_color).run()
2204
2205
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002206def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002207 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002208 CLANG_EXTS = ['.cc', '.cpp', '.h']
2209 parser.add_option('--full', action='store_true', default=False)
2210 opts, args = parser.parse_args(args)
2211 if args:
2212 parser.error('Unrecognized args: %s' % ' '.join(args))
2213
digit@chromium.org29e47272013-05-17 17:01:46 +00002214 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002215 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002216 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002217 # Only list the names of modified files.
2218 diff_cmd.append('--name-only')
2219 else:
2220 # Only generate context-less patches.
2221 diff_cmd.append('-U0')
2222
2223 # Grab the merge-base commit, i.e. the upstream commit of the current
2224 # branch when it was created or the last time it was rebased. This is
2225 # to cover the case where the user may have called "git fetch origin",
2226 # moving the origin branch to a newer commit, but hasn't rebased yet.
2227 upstream_commit = None
2228 cl = Changelist()
2229 upstream_branch = cl.GetUpstreamBranch()
2230 if upstream_branch:
2231 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2232 upstream_commit = upstream_commit.strip()
2233
2234 if not upstream_commit:
2235 DieWithError('Could not find base commit for this branch. '
2236 'Are you in detached state?')
2237
2238 diff_cmd.append(upstream_commit)
2239
2240 # Handle source file filtering.
2241 diff_cmd.append('--')
2242 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2243 diff_output = RunGit(diff_cmd)
2244
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002245 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2246
digit@chromium.org29e47272013-05-17 17:01:46 +00002247 if opts.full:
2248 # diff_output is a list of files to send to clang-format.
2249 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002250 if not files:
2251 print "Nothing to format."
2252 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002253 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2254 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002255 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002256 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002257 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2258 'clang-format-diff.py')
2259 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002260 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002261 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002262 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002263
2264 return 0
2265
2266
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002267class OptionParser(optparse.OptionParser):
2268 """Creates the option parse and add --verbose support."""
2269 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002270 optparse.OptionParser.__init__(
2271 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002272 self.add_option(
2273 '-v', '--verbose', action='count', default=0,
2274 help='Use 2 times for more debugging info')
2275
2276 def parse_args(self, args=None, values=None):
2277 options, args = optparse.OptionParser.parse_args(self, args, values)
2278 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2279 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2280 return options, args
2281
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002283def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002284 if sys.hexversion < 0x02060000:
2285 print >> sys.stderr, (
2286 '\nYour python version %s is unsupported, please upgrade.\n' %
2287 sys.version.split(' ', 1)[0])
2288 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002289
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002290 # Reload settings.
2291 global settings
2292 settings = Settings()
2293
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002294 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002295 dispatcher = subcommand.CommandDispatcher(__name__)
2296 try:
2297 return dispatcher.execute(OptionParser(), argv)
2298 except urllib2.HTTPError, e:
2299 if e.code != 500:
2300 raise
2301 DieWithError(
2302 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2303 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002304
2305
2306if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002307 # These affect sys.stdout so do it outside of main() to simplify mocks in
2308 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002309 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002310 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002311 sys.exit(main(sys.argv[1:]))