blob: 1062a7a0fa440ab01ab2ba157c5601d2beecdc57 [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:
445 base_cc = settings .GetDefaultCCList()
446 more_cc = ','.join(self.watchers)
447 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
448 return self.cc
449
450 def SetWatchers(self, watchers):
451 """Set the list of email addresses that should be cc'd based on the changed
452 files in this CL.
453 """
454 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000455
456 def GetBranch(self):
457 """Returns the short branch name, e.g. 'master'."""
458 if not self.branch:
459 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
460 self.branch = ShortBranchName(self.branchref)
461 return self.branch
462
463 def GetBranchRef(self):
464 """Returns the full branch name, e.g. 'refs/heads/master'."""
465 self.GetBranch() # Poke the lazy loader.
466 return self.branchref
467
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000468 @staticmethod
469 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470 """Returns a tuple containg remote and remote ref,
471 e.g. 'origin', 'refs/heads/master'
472 """
473 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000474 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
475 error_ok=True).strip()
476 if upstream_branch:
477 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
478 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000479 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
480 error_ok=True).strip()
481 if upstream_branch:
482 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000483 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000484 # Fall back on trying a git-svn upstream branch.
485 if settings.GetIsGitSvn():
486 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000487 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000488 # Else, try to guess the origin remote.
489 remote_branches = RunGit(['branch', '-r']).split()
490 if 'origin/master' in remote_branches:
491 # Fall back on origin/master if it exits.
492 remote = 'origin'
493 upstream_branch = 'refs/heads/master'
494 elif 'origin/trunk' in remote_branches:
495 # Fall back on origin/trunk if it exists. Generally a shared
496 # git-svn clone
497 remote = 'origin'
498 upstream_branch = 'refs/heads/trunk'
499 else:
500 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000501Either pass complete "git diff"-style arguments, like
502 git cl upload origin/master
503or verify this branch is set up to track another (via the --track argument to
504"git checkout -b ...").""")
505
506 return remote, upstream_branch
507
508 def GetUpstreamBranch(self):
509 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000510 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000511 if remote is not '.':
512 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
513 self.upstream_branch = upstream_branch
514 return self.upstream_branch
515
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000517 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000518 remote, branch = None, self.GetBranch()
519 seen_branches = set()
520 while branch not in seen_branches:
521 seen_branches.add(branch)
522 remote, branch = self.FetchUpstreamTuple(branch)
523 branch = ShortBranchName(branch)
524 if remote != '.' or branch.startswith('refs/remotes'):
525 break
526 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000527 remotes = RunGit(['remote'], error_ok=True).split()
528 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000529 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000530 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000531 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000532 logging.warning('Could not determine which remote this change is '
533 'associated with, so defaulting to "%s". This may '
534 'not be what you want. You may prevent this message '
535 'by running "git svn info" as documented here: %s',
536 self._remote,
537 GIT_INSTRUCTIONS_URL)
538 else:
539 logging.warn('Could not determine which remote this change is '
540 'associated with. You may prevent this message by '
541 'running "git svn info" as documented here: %s',
542 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000543 branch = 'HEAD'
544 if branch.startswith('refs/remotes'):
545 self._remote = (remote, branch)
546 else:
547 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000548 return self._remote
549
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000550 def GitSanityChecks(self, upstream_git_obj):
551 """Checks git repo status and ensures diff is from local commits."""
552
553 # Verify the commit we're diffing against is in our current branch.
554 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
555 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
556 if upstream_sha != common_ancestor:
557 print >> sys.stderr, (
558 'ERROR: %s is not in the current branch. You may need to rebase '
559 'your tracking branch' % upstream_sha)
560 return False
561
562 # List the commits inside the diff, and verify they are all local.
563 commits_in_diff = RunGit(
564 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
565 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
566 remote_branch = remote_branch.strip()
567 if code != 0:
568 _, remote_branch = self.GetRemoteBranch()
569
570 commits_in_remote = RunGit(
571 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
572
573 common_commits = set(commits_in_diff) & set(commits_in_remote)
574 if common_commits:
575 print >> sys.stderr, (
576 'ERROR: Your diff contains %d commits already in %s.\n'
577 'Run "git log --oneline %s..HEAD" to get a list of commits in '
578 'the diff. If you are using a custom git flow, you can override'
579 ' the reference used for this check with "git config '
580 'gitcl.remotebranch <git-ref>".' % (
581 len(common_commits), remote_branch, upstream_git_obj))
582 return False
583 return True
584
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000585 def GetGitBaseUrlFromConfig(self):
586 """Return the configured base URL from branch.<branchname>.baseurl.
587
588 Returns None if it is not set.
589 """
590 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
591 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000592
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000593 def GetRemoteUrl(self):
594 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
595
596 Returns None if there is no remote.
597 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000598 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
600
601 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000602 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000603 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000605 self.issue = int(issue) or None if issue else None
606 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 return self.issue
608
609 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000610 if not self.rietveld_server:
611 # If we're on a branch then get the server potentially associated
612 # with that branch.
613 if self.GetIssue():
614 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
615 ['config', self._RietveldServer()], error_ok=True).strip())
616 if not self.rietveld_server:
617 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618 return self.rietveld_server
619
620 def GetIssueURL(self):
621 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000622 if not self.GetIssue():
623 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
625
626 def GetDescription(self, pretty=False):
627 if not self.has_description:
628 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000629 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000630 try:
631 self.description = self.RpcServer().get_description(issue).strip()
632 except urllib2.HTTPError, e:
633 if e.code == 404:
634 DieWithError(
635 ('\nWhile fetching the description for issue %d, received a '
636 '404 (not found)\n'
637 'error. It is likely that you deleted this '
638 'issue on the server. If this is the\n'
639 'case, please run\n\n'
640 ' git cl issue 0\n\n'
641 'to clear the association with the deleted issue. Then run '
642 'this command again.') % issue)
643 else:
644 DieWithError(
645 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 self.has_description = True
647 if pretty:
648 wrapper = textwrap.TextWrapper()
649 wrapper.initial_indent = wrapper.subsequent_indent = ' '
650 return wrapper.fill(self.description)
651 return self.description
652
653 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000654 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000655 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 patchset = RunGit(['config', self._PatchsetSetting()],
657 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000658 self.patchset = int(patchset) or None if patchset else None
659 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 return self.patchset
661
662 def SetPatchset(self, patchset):
663 """Set this branch's patchset. If patchset=0, clears the patchset."""
664 if patchset:
665 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000666 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667 else:
668 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000669 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000670 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 def GetMostRecentPatchset(self):
673 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000674
675 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000676 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000677 '/download/issue%s_%s.diff' % (issue, patchset))
678
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000679 def GetIssueProperties(self):
680 if self._props is None:
681 issue = self.GetIssue()
682 if not issue:
683 self._props = {}
684 else:
685 self._props = self.RpcServer().get_issue_properties(issue, True)
686 return self._props
687
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000688 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000689 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 def SetIssue(self, issue):
692 """Set this branch's issue. If issue=0, clears the issue."""
693 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000694 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 RunGit(['config', self._IssueSetting(), str(issue)])
696 if self.rietveld_server:
697 RunGit(['config', self._RietveldServer(), self.rietveld_server])
698 else:
699 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000700 self.issue = None
701 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000703 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000704 if not self.GitSanityChecks(upstream_branch):
705 DieWithError('\nGit sanity check failure')
706
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000707 env = os.environ.copy()
708 # 'cat' is a magical git string that disables pagers on all platforms.
709 env['GIT_PAGER'] = 'cat'
710
711 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000712 if not root:
713 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000714 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000715
716 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000717 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000718 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000719 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000720 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000721 except subprocess2.CalledProcessError:
722 DieWithError(
723 ('\nFailed to diff against upstream branch %s!\n\n'
724 'This branch probably doesn\'t exist anymore. To reset the\n'
725 'tracking branch, please run\n'
726 ' git branch --set-upstream %s trunk\n'
727 'replacing trunk with origin/master or the relevant branch') %
728 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000729
maruel@chromium.org52424302012-08-29 15:14:30 +0000730 issue = self.GetIssue()
731 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000732 if issue:
733 description = self.GetDescription()
734 else:
735 # If the change was never uploaded, use the log messages of all commits
736 # up to the branch point, as git cl upload will prefill the description
737 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000738 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000739 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000740 '%s...' % (upstream_branch)],
741 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000742
743 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000744 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000745 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000746 name,
747 description,
748 absroot,
749 files,
750 issue,
751 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000752 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000753
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000754 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000755 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000756
757 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000758 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000759 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000760 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000761 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000762 except presubmit_support.PresubmitFailure, e:
763 DieWithError(
764 ('%s\nMaybe your depot_tools is out of date?\n'
765 'If all fails, contact maruel@') % e)
766
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000767 def UpdateDescription(self, description):
768 self.description = description
769 return self.RpcServer().update_description(
770 self.GetIssue(), self.description)
771
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000773 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000774 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000776 def SetFlag(self, flag, value):
777 """Patchset must match."""
778 if not self.GetPatchset():
779 DieWithError('The patchset needs to match. Send another patchset.')
780 try:
781 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000782 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000783 except urllib2.HTTPError, e:
784 if e.code == 404:
785 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
786 if e.code == 403:
787 DieWithError(
788 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
789 'match?') % (self.GetIssue(), self.GetPatchset()))
790 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000792 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 """Returns an upload.RpcServer() to access this review's rietveld instance.
794 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000795 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000796 self._rpc_server = rietveld.CachingRietveld(
797 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000798 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799
800 def _IssueSetting(self):
801 """Return the git setting that stores this change's issue."""
802 return 'branch.%s.rietveldissue' % self.GetBranch()
803
804 def _PatchsetSetting(self):
805 """Return the git setting that stores this change's most recent patchset."""
806 return 'branch.%s.rietveldpatchset' % self.GetBranch()
807
808 def _RietveldServer(self):
809 """Returns the git setting that stores this change's rietveld server."""
810 return 'branch.%s.rietveldserver' % self.GetBranch()
811
812
813def GetCodereviewSettingsInteractively():
814 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000815 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816 server = settings.GetDefaultServerUrl(error_ok=True)
817 prompt = 'Rietveld server (host[:port])'
818 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000819 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000820 if not server and not newserver:
821 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000822 if newserver:
823 newserver = gclient_utils.UpgradeToHttps(newserver)
824 if newserver != server:
825 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000827 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 prompt = caption
829 if initial:
830 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000831 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 if new_val == 'x':
833 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000834 elif new_val:
835 if is_url:
836 new_val = gclient_utils.UpgradeToHttps(new_val)
837 if new_val != initial:
838 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000840 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000841 SetProperty(settings.GetDefaultPrivateFlag(),
842 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000844 'tree-status-url', False)
845 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846
847 # TODO: configure a default branch to diff against, rather than this
848 # svn-based hackery.
849
850
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000851class ChangeDescription(object):
852 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000853 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000854 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000855
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000856 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000857 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000858
agable@chromium.org42c20792013-09-12 17:34:49 +0000859 @property # www.logilab.org/ticket/89786
860 def description(self): # pylint: disable=E0202
861 return '\n'.join(self._description_lines)
862
863 def set_description(self, desc):
864 if isinstance(desc, basestring):
865 lines = desc.splitlines()
866 else:
867 lines = [line.rstrip() for line in desc]
868 while lines and not lines[0]:
869 lines.pop(0)
870 while lines and not lines[-1]:
871 lines.pop(-1)
872 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000873
874 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000875 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000876 assert isinstance(reviewers, list), reviewers
877 if not reviewers:
878 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000879 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880
agable@chromium.org42c20792013-09-12 17:34:49 +0000881 # Get the set of R= and TBR= lines and remove them from the desciption.
882 regexp = re.compile(self.R_LINE)
883 matches = [regexp.match(line) for line in self._description_lines]
884 new_desc = [l for i, l in enumerate(self._description_lines)
885 if not matches[i]]
886 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000887
agable@chromium.org42c20792013-09-12 17:34:49 +0000888 # Construct new unified R= and TBR= lines.
889 r_names = []
890 tbr_names = []
891 for match in matches:
892 if not match:
893 continue
894 people = cleanup_list([match.group(2).strip()])
895 if match.group(1) == 'TBR':
896 tbr_names.extend(people)
897 else:
898 r_names.extend(people)
899 for name in r_names:
900 if name not in reviewers:
901 reviewers.append(name)
902 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
903 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
904
905 # Put the new lines in the description where the old first R= line was.
906 line_loc = next((i for i, match in enumerate(matches) if match), -1)
907 if 0 <= line_loc < len(self._description_lines):
908 if new_tbr_line:
909 self._description_lines.insert(line_loc, new_tbr_line)
910 if new_r_line:
911 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000912 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000913 if new_r_line:
914 self.append_footer(new_r_line)
915 if new_tbr_line:
916 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000917
918 def prompt(self):
919 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000920 self.set_description([
921 '# Enter a description of the change.',
922 '# This will be displayed on the codereview site.',
923 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000924 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000925 '--------------------',
926 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000927
agable@chromium.org42c20792013-09-12 17:34:49 +0000928 regexp = re.compile(self.BUG_LINE)
929 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000930 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000931 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000932 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000933 if not content:
934 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000935 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000936
937 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000938 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
939 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000940 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000941 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000942
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000943 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000944 if self._description_lines:
945 # Add an empty line if either the last line or the new line isn't a tag.
946 last_line = self._description_lines[-1]
947 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
948 not presubmit_support.Change.TAG_LINE_RE.match(line)):
949 self._description_lines.append('')
950 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000951
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000952 def get_reviewers(self):
953 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000954 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
955 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000956 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000957
958
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000959def get_approving_reviewers(props):
960 """Retrieves the reviewers that approved a CL from the issue properties with
961 messages.
962
963 Note that the list may contain reviewers that are not committer, thus are not
964 considered by the CQ.
965 """
966 return sorted(
967 set(
968 message['sender']
969 for message in props['messages']
970 if message['approval'] and message['sender'] in props['reviewers']
971 )
972 )
973
974
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975def FindCodereviewSettingsFile(filename='codereview.settings'):
976 """Finds the given file starting in the cwd and going up.
977
978 Only looks up to the top of the repository unless an
979 'inherit-review-settings-ok' file exists in the root of the repository.
980 """
981 inherit_ok_file = 'inherit-review-settings-ok'
982 cwd = os.getcwd()
983 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
984 if os.path.isfile(os.path.join(root, inherit_ok_file)):
985 root = '/'
986 while True:
987 if filename in os.listdir(cwd):
988 if os.path.isfile(os.path.join(cwd, filename)):
989 return open(os.path.join(cwd, filename))
990 if cwd == root:
991 break
992 cwd = os.path.dirname(cwd)
993
994
995def LoadCodereviewSettingsFromFile(fileobj):
996 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000997 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 def SetProperty(name, setting, unset_error_ok=False):
1000 fullname = 'rietveld.' + name
1001 if setting in keyvals:
1002 RunGit(['config', fullname, keyvals[setting]])
1003 else:
1004 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1005
1006 SetProperty('server', 'CODE_REVIEW_SERVER')
1007 # Only server setting is required. Other settings can be absent.
1008 # In that case, we ignore errors raised during option deletion attempt.
1009 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001010 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1012 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1013
ukai@chromium.orge8077812012-02-03 03:41:46 +00001014 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
1015 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
1016 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001017
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1019 #should be of the form
1020 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1021 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1022 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1023 keyvals['ORIGIN_URL_CONFIG']])
1024
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001026def urlretrieve(source, destination):
1027 """urllib is broken for SSL connections via a proxy therefore we
1028 can't use urllib.urlretrieve()."""
1029 with open(destination, 'w') as f:
1030 f.write(urllib2.urlopen(source).read())
1031
1032
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001033def DownloadHooks(force):
1034 """downloads hooks
1035
1036 Args:
1037 force: True to update hooks. False to install hooks if not present.
1038 """
1039 if not settings.GetIsGerrit():
1040 return
1041 server_url = settings.GetDefaultServerUrl()
1042 src = '%s/tools/hooks/commit-msg' % server_url
1043 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1044 if not os.access(dst, os.X_OK):
1045 if os.path.exists(dst):
1046 if not force:
1047 return
1048 os.remove(dst)
1049 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001050 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001051 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1052 except Exception:
1053 if os.path.exists(dst):
1054 os.remove(dst)
1055 DieWithError('\nFailed to download hooks from %s' % src)
1056
1057
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001058@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001060 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001062 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063 if len(args) == 0:
1064 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001065 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066 return 0
1067
1068 url = args[0]
1069 if not url.endswith('codereview.settings'):
1070 url = os.path.join(url, 'codereview.settings')
1071
1072 # Load code review settings and download hooks (if available).
1073 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001074 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 return 0
1076
1077
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001078def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001079 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001080 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1081 branch = ShortBranchName(branchref)
1082 _, args = parser.parse_args(args)
1083 if not args:
1084 print("Current base-url:")
1085 return RunGit(['config', 'branch.%s.base-url' % branch],
1086 error_ok=False).strip()
1087 else:
1088 print("Setting base-url to %s" % args[0])
1089 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1090 error_ok=False).strip()
1091
1092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001094 """Show status of changelists.
1095
1096 Colors are used to tell the state of the CL unless --fast is used:
1097 - Green LGTM'ed
1098 - Blue waiting for review
1099 - Yellow waiting for you to reply to review
1100 - Red not sent for review or broken
1101 - Cyan was committed, branch can be deleted
1102
1103 Also see 'git cl comments'.
1104 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 parser.add_option('--field',
1106 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001107 parser.add_option('-f', '--fast', action='store_true',
1108 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001110 if args:
1111 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001114 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 if options.field.startswith('desc'):
1116 print cl.GetDescription()
1117 elif options.field == 'id':
1118 issueid = cl.GetIssue()
1119 if issueid:
1120 print issueid
1121 elif options.field == 'patch':
1122 patchset = cl.GetPatchset()
1123 if patchset:
1124 print patchset
1125 elif options.field == 'url':
1126 url = cl.GetIssueURL()
1127 if url:
1128 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001129 return 0
1130
1131 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1132 if not branches:
1133 print('No local branch found.')
1134 return 0
1135
1136 changes = (Changelist(branchref=b) for b in branches.splitlines())
1137 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1138 alignment = max(5, max(len(b) for b in branches))
1139 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001140 # Adhoc thread pool to request data concurrently.
1141 output = Queue.Queue()
1142
1143 # Silence upload.py otherwise it becomes unweldly.
1144 upload.verbosity = 0
1145
1146 if not options.fast:
1147 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001148 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001149 c = Changelist(branchref=b)
1150 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001151 props = {}
1152 r = None
1153 if i:
1154 try:
1155 props = c.GetIssueProperties()
1156 r = c.GetApprovingReviewers() if i else None
1157 except urllib2.HTTPError:
1158 # The issue probably doesn't exist anymore.
1159 i += ' (broken)'
1160
1161 msgs = props.get('messages') or []
1162
1163 if not i:
1164 color = Fore.WHITE
1165 elif props.get('closed'):
1166 # Issue is closed.
1167 color = Fore.CYAN
1168 elif r:
1169 # Was LGTM'ed.
1170 color = Fore.GREEN
1171 elif not msgs:
1172 # No message was sent.
1173 color = Fore.RED
1174 elif msgs[-1]['sender'] != props.get('owner_email'):
1175 color = Fore.YELLOW
1176 else:
1177 color = Fore.BLUE
1178 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001179
1180 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1181 for t in threads:
1182 t.daemon = True
1183 t.start()
1184 else:
1185 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1186 for b in branches:
1187 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001188 url = c.GetIssueURL()
1189 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001190
1191 tmp = {}
1192 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001193 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001194 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001195 b, i, color = output.get()
1196 tmp[b] = (i, color)
1197 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001198 reset = Fore.RESET
1199 if not sys.stdout.isatty():
1200 color = ''
1201 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001202 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001203 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001204
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001205 cl = Changelist()
1206 print
1207 print 'Current branch:',
1208 if not cl.GetIssue():
1209 print 'no issue assigned.'
1210 return 0
1211 print cl.GetBranch()
1212 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1213 print 'Issue description:'
1214 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 return 0
1216
1217
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001218def colorize_CMDstatus_doc():
1219 """To be called once in main() to add colors to git cl status help."""
1220 colors = [i for i in dir(Fore) if i[0].isupper()]
1221
1222 def colorize_line(line):
1223 for color in colors:
1224 if color in line.upper():
1225 # Extract whitespaces first and the leading '-'.
1226 indent = len(line) - len(line.lstrip(' ')) + 1
1227 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1228 return line
1229
1230 lines = CMDstatus.__doc__.splitlines()
1231 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1232
1233
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001234@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001236 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237
1238 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001239 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001240 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241
1242 cl = Changelist()
1243 if len(args) > 0:
1244 try:
1245 issue = int(args[0])
1246 except ValueError:
1247 DieWithError('Pass a number to set the issue or none to list it.\n'
1248 'Maybe you want to run git cl status?')
1249 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001250 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 return 0
1252
1253
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001254def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001255 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001256 (_, args) = parser.parse_args(args)
1257 if args:
1258 parser.error('Unsupported argument: %s' % args)
1259
1260 cl = Changelist()
1261 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001262 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001263 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001264 if message['disapproval']:
1265 color = Fore.RED
1266 elif message['approval']:
1267 color = Fore.GREEN
1268 elif message['sender'] == data['owner_email']:
1269 color = Fore.MAGENTA
1270 else:
1271 color = Fore.BLUE
1272 print '\n%s%s %s%s' % (
1273 color, message['date'].split('.', 1)[0], message['sender'],
1274 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001275 if message['text'].strip():
1276 print '\n'.join(' ' + l for l in message['text'].splitlines())
1277 return 0
1278
1279
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001280def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001281 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001282 cl = Changelist()
1283 if not cl.GetIssue():
1284 DieWithError('This branch has no associated changelist.')
1285 description = ChangeDescription(cl.GetDescription())
1286 description.prompt()
1287 cl.UpdateDescription(description.description)
1288 return 0
1289
1290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291def CreateDescriptionFromLog(args):
1292 """Pulls out the commit log to use as a base for the CL description."""
1293 log_args = []
1294 if len(args) == 1 and not args[0].endswith('.'):
1295 log_args = [args[0] + '..']
1296 elif len(args) == 1 and args[0].endswith('...'):
1297 log_args = [args[0][:-1]]
1298 elif len(args) == 2:
1299 log_args = [args[0] + '..' + args[1]]
1300 else:
1301 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001302 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303
1304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001306 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001307 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001309 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001310 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 (options, args) = parser.parse_args(args)
1312
ukai@chromium.org259e4682012-10-25 07:36:33 +00001313 if not options.force and is_dirty_git_tree('presubmit'):
1314 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315 return 1
1316
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001317 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 if args:
1319 base_branch = args[0]
1320 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001321 # Default to diffing against the common ancestor of the upstream branch.
1322 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001324 cl.RunHook(
1325 committing=not options.upload,
1326 may_prompt=False,
1327 verbose=options.verbose,
1328 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001329 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330
1331
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001332def AddChangeIdToCommitMessage(options, args):
1333 """Re-commits using the current message, assumes the commit hook is in
1334 place.
1335 """
1336 log_desc = options.message or CreateDescriptionFromLog(args)
1337 git_command = ['commit', '--amend', '-m', log_desc]
1338 RunGit(git_command)
1339 new_log_desc = CreateDescriptionFromLog(args)
1340 if CHANGE_ID in new_log_desc:
1341 print 'git-cl: Added Change-Id to commit message.'
1342 else:
1343 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1344
1345
ukai@chromium.orge8077812012-02-03 03:41:46 +00001346def GerritUpload(options, args, cl):
1347 """upload the current branch to gerrit."""
1348 # We assume the remote called "origin" is the one we want.
1349 # It is probably not worthwhile to support different workflows.
1350 remote = 'origin'
1351 branch = 'master'
1352 if options.target_branch:
1353 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001355 change_desc = ChangeDescription(
1356 options.message or CreateDescriptionFromLog(args))
1357 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001358 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001360 if CHANGE_ID not in change_desc.description:
1361 AddChangeIdToCommitMessage(options, args)
1362 if options.reviewers:
1363 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364
ukai@chromium.orge8077812012-02-03 03:41:46 +00001365 receive_options = []
1366 cc = cl.GetCCList().split(',')
1367 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001368 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001369 cc = filter(None, cc)
1370 if cc:
1371 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372 if change_desc.get_reviewers():
1373 receive_options.extend(
1374 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375
ukai@chromium.orge8077812012-02-03 03:41:46 +00001376 git_command = ['push']
1377 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001378 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001379 ' '.join(receive_options))
1380 git_command += [remote, 'HEAD:refs/for/' + branch]
1381 RunGit(git_command)
1382 # TODO(ukai): parse Change-Id: and set issue number?
1383 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001384
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385
ukai@chromium.orge8077812012-02-03 03:41:46 +00001386def RietveldUpload(options, args, cl):
1387 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1389 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 if options.emulate_svn_auto_props:
1391 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392
1393 change_desc = None
1394
1395 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001396 if options.title:
1397 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001398 if options.message:
1399 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001400 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 print ("This branch is associated with issue %s. "
1402 "Adding patch to that issue." % cl.GetIssue())
1403 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001404 if options.title:
1405 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001406 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001407 change_desc = ChangeDescription(message)
1408 if options.reviewers:
1409 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001410 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001411 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001412
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001413 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 print "Description is empty; aborting."
1415 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001416
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001417 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001418 if change_desc.get_reviewers():
1419 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001420 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001421 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001422 DieWithError("Must specify reviewers to send email.")
1423 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001424 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001425 if cc:
1426 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001428 if options.private or settings.GetDefaultPrivateFlag() == "True":
1429 upload_args.append('--private')
1430
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001431 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001432 if not options.find_copies:
1433 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001434
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 # Include the upstream repo's URL in the change -- this is useful for
1436 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001437 remote_url = cl.GetGitBaseUrlFromConfig()
1438 if not remote_url:
1439 if settings.GetIsGitSvn():
1440 # URL is dependent on the current directory.
1441 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1442 if data:
1443 keys = dict(line.split(': ', 1) for line in data.splitlines()
1444 if ': ' in line)
1445 remote_url = keys.get('URL', None)
1446 else:
1447 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1448 remote_url = (cl.GetRemoteUrl() + '@'
1449 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 if remote_url:
1451 upload_args.extend(['--base_url', remote_url])
1452
1453 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001454 upload_args = ['upload'] + upload_args + args
1455 logging.info('upload.RealMain(%s)', upload_args)
1456 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001457 issue = int(issue)
1458 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001459 except KeyboardInterrupt:
1460 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 except:
1462 # If we got an exception after the user typed a description for their
1463 # change, back up the description before re-raising.
1464 if change_desc:
1465 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1466 print '\nGot exception while uploading -- saving description to %s\n' \
1467 % backup_path
1468 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001469 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 backup_file.close()
1471 raise
1472
1473 if not cl.GetIssue():
1474 cl.SetIssue(issue)
1475 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001476
1477 if options.use_commit_queue:
1478 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 return 0
1480
1481
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001482def cleanup_list(l):
1483 """Fixes a list so that comma separated items are put as individual items.
1484
1485 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1486 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1487 """
1488 items = sum((i.split(',') for i in l), [])
1489 stripped_items = (i.strip() for i in items)
1490 return sorted(filter(None, stripped_items))
1491
1492
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001493@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001494def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001495 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001496 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1497 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001498 parser.add_option('--bypass-watchlists', action='store_true',
1499 dest='bypass_watchlists',
1500 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001501 parser.add_option('-f', action='store_true', dest='force',
1502 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001503 parser.add_option('-m', dest='message', help='message for patchset')
1504 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001505 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001506 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001507 help='reviewer email addresses')
1508 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001509 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001510 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001511 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001512 help='send email to reviewer immediately')
1513 parser.add_option("--emulate_svn_auto_props", action="store_true",
1514 dest="emulate_svn_auto_props",
1515 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001516 parser.add_option('-c', '--use-commit-queue', action='store_true',
1517 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001518 parser.add_option('--private', action='store_true',
1519 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001520 parser.add_option('--target_branch',
1521 help='When uploading to gerrit, remote branch to '
1522 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001523 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001524 (options, args) = parser.parse_args(args)
1525
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001526 if options.target_branch and not settings.GetIsGerrit():
1527 parser.error('Use --target_branch for non gerrit repository.')
1528
ukai@chromium.org259e4682012-10-25 07:36:33 +00001529 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001530 return 1
1531
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001532 options.reviewers = cleanup_list(options.reviewers)
1533 options.cc = cleanup_list(options.cc)
1534
ukai@chromium.orge8077812012-02-03 03:41:46 +00001535 cl = Changelist()
1536 if args:
1537 # TODO(ukai): is it ok for gerrit case?
1538 base_branch = args[0]
1539 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001540 # Default to diffing against common ancestor of upstream branch
1541 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001542 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001543
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001544 # Apply watchlists on upload.
1545 change = cl.GetChange(base_branch, None)
1546 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1547 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001548 if not options.bypass_watchlists:
1549 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001550
ukai@chromium.orge8077812012-02-03 03:41:46 +00001551 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001552 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001553 may_prompt=not options.force,
1554 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001555 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001556 if not hook_results.should_continue():
1557 return 1
1558 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001559 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001560
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001561 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001562 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001563 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001564 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001565 print ('The last upload made from this repository was patchset #%d but '
1566 'the most recent patchset on the server is #%d.'
1567 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001568 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1569 'from another machine or branch the patch you\'re uploading now '
1570 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001571 ask_for_data('About to upload; enter to confirm.')
1572
iannucci@chromium.org79540052012-10-19 23:15:26 +00001573 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001574 if settings.GetIsGerrit():
1575 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001576 ret = RietveldUpload(options, args, cl)
1577 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001578 git_set_branch_value('last-upload-hash',
1579 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001580
1581 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001582
1583
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001584def IsSubmoduleMergeCommit(ref):
1585 # When submodules are added to the repo, we expect there to be a single
1586 # non-git-svn merge commit at remote HEAD with a signature comment.
1587 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001588 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001589 return RunGit(cmd) != ''
1590
1591
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001592def SendUpstream(parser, args, cmd):
1593 """Common code for CmdPush and CmdDCommit
1594
1595 Squashed commit into a single.
1596 Updates changelog with metadata (e.g. pointer to review).
1597 Pushes/dcommits the code upstream.
1598 Updates review and closes.
1599 """
1600 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1601 help='bypass upload presubmit hook')
1602 parser.add_option('-m', dest='message',
1603 help="override review description")
1604 parser.add_option('-f', action='store_true', dest='force',
1605 help="force yes to questions (don't prompt)")
1606 parser.add_option('-c', dest='contributor',
1607 help="external contributor for patch (appended to " +
1608 "description and used as author for git). Should be " +
1609 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001610 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611 (options, args) = parser.parse_args(args)
1612 cl = Changelist()
1613
1614 if not args or cmd == 'push':
1615 # Default to merging against our best guess of the upstream branch.
1616 args = [cl.GetUpstreamBranch()]
1617
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001618 if options.contributor:
1619 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1620 print "Please provide contibutor as 'First Last <email@example.com>'"
1621 return 1
1622
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001624 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001625
ukai@chromium.org259e4682012-10-25 07:36:33 +00001626 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001627 return 1
1628
1629 # This rev-list syntax means "show all commits not in my branch that
1630 # are in base_branch".
1631 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1632 base_branch]).splitlines()
1633 if upstream_commits:
1634 print ('Base branch "%s" has %d commits '
1635 'not in this branch.' % (base_branch, len(upstream_commits)))
1636 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1637 return 1
1638
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001639 # This is the revision `svn dcommit` will commit on top of.
1640 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1641 '--pretty=format:%H'])
1642
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001644 # If the base_head is a submodule merge commit, the first parent of the
1645 # base_head should be a git-svn commit, which is what we're interested in.
1646 base_svn_head = base_branch
1647 if base_has_submodules:
1648 base_svn_head += '^1'
1649
1650 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 if extra_commits:
1652 print ('This branch has %d additional commits not upstreamed yet.'
1653 % len(extra_commits.splitlines()))
1654 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1655 'before attempting to %s.' % (base_branch, cmd))
1656 return 1
1657
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001658 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001659 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001660 author = None
1661 if options.contributor:
1662 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001663 hook_results = cl.RunHook(
1664 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001665 may_prompt=not options.force,
1666 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001667 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001668 if not hook_results.should_continue():
1669 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001670
1671 if cmd == 'dcommit':
1672 # Check the tree status if the tree status URL is set.
1673 status = GetTreeStatus()
1674 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001675 print('The tree is closed. Please wait for it to reopen. Use '
1676 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677 return 1
1678 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001679 print('Unable to determine tree status. Please verify manually and '
1680 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001681 else:
1682 breakpad.SendStack(
1683 'GitClHooksBypassedCommit',
1684 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001685 (cl.GetRietveldServer(), cl.GetIssue()),
1686 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001687
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001688 change_desc = ChangeDescription(options.message)
1689 if not change_desc.description and cl.GetIssue():
1690 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001692 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001693 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001694 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001695 else:
1696 print 'No description set.'
1697 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1698 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001699
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001700 # Keep a separate copy for the commit message, because the commit message
1701 # contains the link to the Rietveld issue, while the Rietveld message contains
1702 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001703 # Keep a separate copy for the commit message.
1704 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001705 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001706
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001707 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001708 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001709 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001711 commit_desc.append_footer('Patch from %s.' % options.contributor)
1712
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001713 print('Description:')
1714 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001715
1716 branches = [base_branch, cl.GetBranchRef()]
1717 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001718 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001719 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001721 # We want to squash all this branch's commits into one commit with the proper
1722 # description. We do this by doing a "reset --soft" to the base branch (which
1723 # keeps the working copy the same), then dcommitting that. If origin/master
1724 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1725 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001727 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1728 # Delete the branches if they exist.
1729 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1730 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1731 result = RunGitWithCode(showref_cmd)
1732 if result[0] == 0:
1733 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001734
1735 # We might be in a directory that's present in this branch but not in the
1736 # trunk. Move up to the top of the tree so that git commands that expect a
1737 # valid CWD won't fail after we check out the merge branch.
1738 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1739 if rel_base_path:
1740 os.chdir(rel_base_path)
1741
1742 # Stuff our change into the merge branch.
1743 # We wrap in a try...finally block so if anything goes wrong,
1744 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001745 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001747 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1748 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001750 RunGit(
1751 [
1752 'commit', '--author', options.contributor,
1753 '-m', commit_desc.description,
1754 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001756 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001757 if base_has_submodules:
1758 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1759 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1760 RunGit(['checkout', CHERRY_PICK_BRANCH])
1761 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762 if cmd == 'push':
1763 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001764 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 retcode, output = RunGitWithCode(
1766 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1767 logging.debug(output)
1768 else:
1769 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001770 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001771 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001772 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773 finally:
1774 # And then swap back to the original branch and clean up.
1775 RunGit(['checkout', '-q', cl.GetBranch()])
1776 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001777 if base_has_submodules:
1778 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779
1780 if cl.GetIssue():
1781 if cmd == 'dcommit' and 'Committed r' in output:
1782 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1783 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001784 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1785 for l in output.splitlines(False))
1786 match = filter(None, match)
1787 if len(match) != 1:
1788 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1789 output)
1790 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 else:
1792 return 1
1793 viewvc_url = settings.GetViewVCUrl()
1794 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001795 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001796 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001797 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001798 print ('Closing issue '
1799 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001800 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001802 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001803 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001804 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001805 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1806 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001807 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001808
1809 if retcode == 0:
1810 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1811 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001812 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814 return 0
1815
1816
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001817@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001819 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001821 message = """This doesn't appear to be an SVN repository.
1822If your project has a git mirror with an upstream SVN master, you probably need
1823to run 'git svn init', see your project's git mirror documentation.
1824If your project has a true writeable upstream repository, you probably want
1825to run 'git cl push' instead.
1826Choose wisely, if you get this wrong, your commit might appear to succeed but
1827will instead be silently ignored."""
1828 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001829 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830 return SendUpstream(parser, args, 'dcommit')
1831
1832
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001833@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001835 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 if settings.GetIsGitSvn():
1837 print('This appears to be an SVN repository.')
1838 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001839 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 return SendUpstream(parser, args, 'push')
1841
1842
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001843@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844def CMDpatch(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001845 """Patchs in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 parser.add_option('-b', dest='newbranch',
1847 help='create a new branch off trunk for the patch')
1848 parser.add_option('-f', action='store_true', dest='force',
1849 help='with -b, clobber any existing branch')
1850 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001851 help='failed patches spew .rej files rather than '
1852 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1854 help="don't commit after patch applies")
1855 (options, args) = parser.parse_args(args)
1856 if len(args) != 1:
1857 parser.print_help()
1858 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001859 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001861 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001862 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001863
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001864 if options.newbranch:
1865 if options.force:
1866 RunGit(['branch', '-D', options.newbranch],
1867 stderr=subprocess2.PIPE, error_ok=True)
1868 RunGit(['checkout', '-b', options.newbranch,
1869 Changelist().GetUpstreamBranch()])
1870
1871 return PatchIssue(issue_arg, options.reject, options.nocommit)
1872
1873
1874def PatchIssue(issue_arg, reject, nocommit):
1875 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001877 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001878 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001879 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001880 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001882 # Assume it's a URL to the patch. Default to https.
1883 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001884 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001885 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886 DieWithError('Must pass an issue ID or full URL for '
1887 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001888 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001889 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001890 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892 # Switch up to the top-level directory, if necessary, in preparation for
1893 # applying the patch.
1894 top = RunGit(['rev-parse', '--show-cdup']).strip()
1895 if top:
1896 os.chdir(top)
1897
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898 # Git patches have a/ at the beginning of source paths. We strip that out
1899 # with a sed script rather than the -p flag to patch so we can feed either
1900 # Git or svn-style patches into the same apply command.
1901 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001902 try:
1903 patch_data = subprocess2.check_output(
1904 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1905 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906 DieWithError('Git patch mungling failed.')
1907 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001908 env = os.environ.copy()
1909 # 'cat' is a magical git string that disables pagers on all platforms.
1910 env['GIT_PAGER'] = 'cat'
1911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912 # We use "git apply" to apply the patch instead of "patch" so that we can
1913 # pick up file adds.
1914 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001915 cmd = ['git', 'apply', '--index', '-p0']
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001916 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001917 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001918 elif IsGitVersionAtLeast('1.7.12'):
1919 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001920 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001921 subprocess2.check_call(cmd, env=env,
1922 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001923 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924 DieWithError('Failed to apply the patch')
1925
1926 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001927 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1929 cl = Changelist()
1930 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001931 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001932 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 else:
1934 print "Patch applied to index."
1935 return 0
1936
1937
1938def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001939 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940 # Provide a wrapper for git svn rebase to help avoid accidental
1941 # git svn dcommit.
1942 # It's the only command that doesn't use parser at all since we just defer
1943 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001944 env = os.environ.copy()
1945 # 'cat' is a magical git string that disables pagers on all platforms.
1946 env['GIT_PAGER'] = 'cat'
1947
1948 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949
1950
1951def GetTreeStatus():
1952 """Fetches the tree status and returns either 'open', 'closed',
1953 'unknown' or 'unset'."""
1954 url = settings.GetTreeStatusUrl(error_ok=True)
1955 if url:
1956 status = urllib2.urlopen(url).read().lower()
1957 if status.find('closed') != -1 or status == '0':
1958 return 'closed'
1959 elif status.find('open') != -1 or status == '1':
1960 return 'open'
1961 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 return 'unset'
1963
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001964
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965def GetTreeStatusReason():
1966 """Fetches the tree status from a json url and returns the message
1967 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001968 url = settings.GetTreeStatusUrl()
1969 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970 connection = urllib2.urlopen(json_url)
1971 status = json.loads(connection.read())
1972 connection.close()
1973 return status['message']
1974
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001975
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001977 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001978 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 status = GetTreeStatus()
1980 if 'unset' == status:
1981 print 'You must configure your tree status URL by running "git cl config".'
1982 return 2
1983
1984 print "The tree is %s" % status
1985 print
1986 print GetTreeStatusReason()
1987 if status != 'open':
1988 return 1
1989 return 0
1990
1991
maruel@chromium.org15192402012-09-06 12:38:29 +00001992def CMDtry(parser, args):
1993 """Triggers a try job through Rietveld."""
1994 group = optparse.OptionGroup(parser, "Try job options")
1995 group.add_option(
1996 "-b", "--bot", action="append",
1997 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1998 "times to specify multiple builders. ex: "
1999 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2000 "the try server waterfall for the builders name and the tests "
2001 "available. Can also be used to specify gtest_filter, e.g. "
2002 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2003 group.add_option(
2004 "-r", "--revision",
2005 help="Revision to use for the try job; default: the "
2006 "revision will be determined by the try server; see "
2007 "its waterfall for more info")
2008 group.add_option(
2009 "-c", "--clobber", action="store_true", default=False,
2010 help="Force a clobber before building; e.g. don't do an "
2011 "incremental build")
2012 group.add_option(
2013 "--project",
2014 help="Override which project to use. Projects are defined "
2015 "server-side to define what default bot set to use")
2016 group.add_option(
2017 "-t", "--testfilter", action="append", default=[],
2018 help=("Apply a testfilter to all the selected builders. Unless the "
2019 "builders configurations are similar, use multiple "
2020 "--bot <builder>:<test> arguments."))
2021 group.add_option(
2022 "-n", "--name", help="Try job name; default to current branch name")
2023 parser.add_option_group(group)
2024 options, args = parser.parse_args(args)
2025
2026 if args:
2027 parser.error('Unknown arguments: %s' % args)
2028
2029 cl = Changelist()
2030 if not cl.GetIssue():
2031 parser.error('Need to upload first')
2032
2033 if not options.name:
2034 options.name = cl.GetBranch()
2035
2036 # Process --bot and --testfilter.
2037 if not options.bot:
2038 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002039 change = cl.GetChange(
2040 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2041 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002042 options.bot = presubmit_support.DoGetTrySlaves(
2043 change,
2044 change.LocalPaths(),
2045 settings.GetRoot(),
2046 None,
2047 None,
2048 options.verbose,
2049 sys.stdout)
2050 if not options.bot:
2051 parser.error('No default try builder to try, use --bot')
2052
2053 builders_and_tests = {}
2054 for bot in options.bot:
2055 if ':' in bot:
2056 builder, tests = bot.split(':', 1)
2057 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2058 elif ',' in bot:
2059 parser.error('Specify one bot per --bot flag')
2060 else:
2061 builders_and_tests.setdefault(bot, []).append('defaulttests')
2062
2063 if options.testfilter:
2064 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2065 builders_and_tests = dict(
2066 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2067 if t != ['compile'])
2068
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002069 if any('triggered' in b for b in builders_and_tests):
2070 print >> sys.stderr, (
2071 'ERROR You are trying to send a job to a triggered bot. This type of'
2072 ' bot requires an\ninitial job from a parent (usually a builder). '
2073 'Instead send your job to the parent.\n'
2074 'Bot list: %s' % builders_and_tests)
2075 return 1
2076
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002077 patchset = cl.GetMostRecentPatchset()
2078 if patchset and patchset != cl.GetPatchset():
2079 print(
2080 '\nWARNING Mismatch between local config and server. Did a previous '
2081 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2082 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002083
2084 cl.RpcServer().trigger_try_jobs(
2085 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2086 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002087 print('Tried jobs on:')
2088 length = max(len(builder) for builder in builders_and_tests)
2089 for builder in sorted(builders_and_tests):
2090 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002091 return 0
2092
2093
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002094@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002095def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002096 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002097 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002098 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002099 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002100 return 0
2101
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002103 if args:
2104 # One arg means set upstream branch.
2105 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2106 cl = Changelist()
2107 print "Upstream branch set to " + cl.GetUpstreamBranch()
2108 else:
2109 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002110 return 0
2111
2112
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002113def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002114 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002115 _, args = parser.parse_args(args)
2116 if args:
2117 parser.error('Unrecognized args: %s' % ' '.join(args))
2118 cl = Changelist()
2119 cl.SetFlag('commit', '1')
2120 return 0
2121
2122
groby@chromium.org411034a2013-02-26 15:12:01 +00002123def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002124 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002125 _, args = parser.parse_args(args)
2126 if args:
2127 parser.error('Unrecognized args: %s' % ' '.join(args))
2128 cl = Changelist()
2129 # Ensure there actually is an issue to close.
2130 cl.GetDescription()
2131 cl.CloseIssue()
2132 return 0
2133
2134
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002135def CMDdiff(parser, args):
2136 """shows differences between local tree and last upload."""
2137 cl = Changelist()
2138 branch = cl.GetBranch()
2139 TMP_BRANCH = 'git-cl-diff'
2140 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2141
2142 # Create a new branch based on the merge-base
2143 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2144 try:
2145 # Patch in the latest changes from rietveld.
2146 rtn = PatchIssue(cl.GetIssue(), False, False)
2147 if rtn != 0:
2148 return rtn
2149
2150 # Switch back to starting brand and diff against the temporary
2151 # branch containing the latest rietveld patch.
2152 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2153 finally:
2154 RunGit(['checkout', '-q', branch])
2155 RunGit(['branch', '-D', TMP_BRANCH])
2156
2157 return 0
2158
2159
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002160def CMDowners(parser, args):
2161 """interactively find the owners for reviewing"""
2162 parser.add_option(
2163 '--no-color',
2164 action='store_true',
2165 help='Use this option to disable color output')
2166 options, args = parser.parse_args(args)
2167
2168 author = RunGit(['config', 'user.email']).strip() or None
2169
2170 cl = Changelist()
2171
2172 if args:
2173 if len(args) > 1:
2174 parser.error('Unknown args')
2175 base_branch = args[0]
2176 else:
2177 # Default to diffing against the common ancestor of the upstream branch.
2178 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2179
2180 change = cl.GetChange(base_branch, None)
2181 return owners_finder.OwnersFinder(
2182 [f.LocalPath() for f in
2183 cl.GetChange(base_branch, None).AffectedFiles()],
2184 change.RepositoryRoot(), author,
2185 fopen=file, os_path=os.path, glob=glob.glob,
2186 disable_color=options.no_color).run()
2187
2188
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002189def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002190 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002191 CLANG_EXTS = ['.cc', '.cpp', '.h']
2192 parser.add_option('--full', action='store_true', default=False)
2193 opts, args = parser.parse_args(args)
2194 if args:
2195 parser.error('Unrecognized args: %s' % ' '.join(args))
2196
digit@chromium.org29e47272013-05-17 17:01:46 +00002197 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002198 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002199 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002200 # Only list the names of modified files.
2201 diff_cmd.append('--name-only')
2202 else:
2203 # Only generate context-less patches.
2204 diff_cmd.append('-U0')
2205
2206 # Grab the merge-base commit, i.e. the upstream commit of the current
2207 # branch when it was created or the last time it was rebased. This is
2208 # to cover the case where the user may have called "git fetch origin",
2209 # moving the origin branch to a newer commit, but hasn't rebased yet.
2210 upstream_commit = None
2211 cl = Changelist()
2212 upstream_branch = cl.GetUpstreamBranch()
2213 if upstream_branch:
2214 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2215 upstream_commit = upstream_commit.strip()
2216
2217 if not upstream_commit:
2218 DieWithError('Could not find base commit for this branch. '
2219 'Are you in detached state?')
2220
2221 diff_cmd.append(upstream_commit)
2222
2223 # Handle source file filtering.
2224 diff_cmd.append('--')
2225 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2226 diff_output = RunGit(diff_cmd)
2227
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002228 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2229
digit@chromium.org29e47272013-05-17 17:01:46 +00002230 if opts.full:
2231 # diff_output is a list of files to send to clang-format.
2232 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002233 if not files:
2234 print "Nothing to format."
2235 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002236 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2237 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002238 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002239 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002240 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2241 'clang-format-diff.py')
2242 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002243 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2244 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002245 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002246
2247 return 0
2248
2249
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002250class OptionParser(optparse.OptionParser):
2251 """Creates the option parse and add --verbose support."""
2252 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002253 optparse.OptionParser.__init__(
2254 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002255 self.add_option(
2256 '-v', '--verbose', action='count', default=0,
2257 help='Use 2 times for more debugging info')
2258
2259 def parse_args(self, args=None, values=None):
2260 options, args = optparse.OptionParser.parse_args(self, args, values)
2261 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2262 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2263 return options, args
2264
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002265
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002266def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002267 if sys.hexversion < 0x02060000:
2268 print >> sys.stderr, (
2269 '\nYour python version %s is unsupported, please upgrade.\n' %
2270 sys.version.split(' ', 1)[0])
2271 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002272
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002273 # Reload settings.
2274 global settings
2275 settings = Settings()
2276
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002277 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002278 dispatcher = subcommand.CommandDispatcher(__name__)
2279 try:
2280 return dispatcher.execute(OptionParser(), argv)
2281 except urllib2.HTTPError, e:
2282 if e.code != 500:
2283 raise
2284 DieWithError(
2285 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2286 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002287
2288
2289if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002290 # These affect sys.stdout so do it outside of main() to simplify mocks in
2291 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002292 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002293 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002294 sys.exit(main(sys.argv[1:]))