blob: 6de71b007928669af44871554d2a311eb75edf71 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024
25try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000026 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027except ImportError:
28 pass
29
maruel@chromium.org2a74d372011-03-29 19:05:50 +000030
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000031from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032from third_party import upload
33import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000034import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000035import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000037import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000039import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000040import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000042import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043
maruel@chromium.org0633fb42013-08-16 20:06:14 +000044__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000046DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000047POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000049GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000050CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000052# Shortcut since it quickly becomes redundant.
53Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000054
maruel@chromium.orgddd59412011-11-30 14:20:38 +000055# Initialized in main()
56settings = None
57
58
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000060 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 sys.exit(1)
62
63
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000064def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000066 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000067 except subprocess2.CalledProcessError as e:
68 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000069 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 'Command "%s" failed.\n%s' % (
72 ' '.join(args), error_message or e.stdout or ''))
73 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000078 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
80
81def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000082 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000083 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000084 env = os.environ.copy()
85 # 'cat' is a magical git string that disables pagers on all platforms.
86 env['GIT_PAGER'] = 'cat'
87 out, code = subprocess2.communicate(['git'] + args,
88 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000089 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000090 return code, out[0]
91 except ValueError:
92 # When the subprocess fails, it returns None. That triggers a ValueError
93 # when trying to unpack the return value into (out, code).
94 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000095
96
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000097def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000098 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000099 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000100 return (version.startswith(prefix) and
101 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000102
103
maruel@chromium.org90541732011-04-01 17:54:18 +0000104def ask_for_data(prompt):
105 try:
106 return raw_input(prompt)
107 except KeyboardInterrupt:
108 # Hide the exception.
109 sys.exit(1)
110
111
iannucci@chromium.org79540052012-10-19 23:15:26 +0000112def git_set_branch_value(key, value):
113 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000114 if not branch:
115 return
116
117 cmd = ['config']
118 if isinstance(value, int):
119 cmd.append('--int')
120 git_key = 'branch.%s.%s' % (branch, key)
121 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000122
123
124def git_get_branch_default(key, default):
125 branch = Changelist().GetBranch()
126 if branch:
127 git_key = 'branch.%s.%s' % (branch, key)
128 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
129 try:
130 return int(stdout.strip())
131 except ValueError:
132 pass
133 return default
134
135
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000136def add_git_similarity(parser):
137 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000139 help='Sets the percentage that a pair of files need to match in order to'
140 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000141 parser.add_option(
142 '--find-copies', action='store_true',
143 help='Allows git to look for copies.')
144 parser.add_option(
145 '--no-find-copies', action='store_false', dest='find_copies',
146 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000147
148 old_parser_args = parser.parse_args
149 def Parse(args):
150 options, args = old_parser_args(args)
151
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000153 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000154 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 print('Note: Saving similarity of %d%% in git config.'
156 % options.similarity)
157 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000158
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159 options.similarity = max(0, min(options.similarity, 100))
160
161 if options.find_copies is None:
162 options.find_copies = bool(
163 git_get_branch_default('git-find-copies', True))
164 else:
165 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000166
167 print('Using %d%% similarity for rename/copy detection. '
168 'Override with --similarity.' % options.similarity)
169
170 return options, args
171 parser.parse_args = Parse
172
173
ukai@chromium.org259e4682012-10-25 07:36:33 +0000174def is_dirty_git_tree(cmd):
175 # Make sure index is up-to-date before running diff-index.
176 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
177 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
178 if dirty:
179 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
180 print 'Uncommitted files: (git diff-index --name-status HEAD)'
181 print dirty[:4096]
182 if len(dirty) > 4096:
183 print '... (run "git diff-index --name-status HEAD" to see full output).'
184 return True
185 return False
186
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000187
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000188def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
189 """Return the corresponding git ref if |base_url| together with |glob_spec|
190 matches the full |url|.
191
192 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
193 """
194 fetch_suburl, as_ref = glob_spec.split(':')
195 if allow_wildcards:
196 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
197 if glob_match:
198 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
199 # "branches/{472,597,648}/src:refs/remotes/svn/*".
200 branch_re = re.escape(base_url)
201 if glob_match.group(1):
202 branch_re += '/' + re.escape(glob_match.group(1))
203 wildcard = glob_match.group(2)
204 if wildcard == '*':
205 branch_re += '([^/]*)'
206 else:
207 # Escape and replace surrounding braces with parentheses and commas
208 # with pipe symbols.
209 wildcard = re.escape(wildcard)
210 wildcard = re.sub('^\\\\{', '(', wildcard)
211 wildcard = re.sub('\\\\,', '|', wildcard)
212 wildcard = re.sub('\\\\}$', ')', wildcard)
213 branch_re += wildcard
214 if glob_match.group(3):
215 branch_re += re.escape(glob_match.group(3))
216 match = re.match(branch_re, url)
217 if match:
218 return re.sub('\*$', match.group(1), as_ref)
219
220 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
221 if fetch_suburl:
222 full_url = base_url + '/' + fetch_suburl
223 else:
224 full_url = base_url
225 if full_url == url:
226 return as_ref
227 return None
228
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000229
iannucci@chromium.org79540052012-10-19 23:15:26 +0000230def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000231 """Prints statistics about the change to the user."""
232 # --no-ext-diff is broken in some versions of Git, so try to work around
233 # this by overriding the environment (but there is still a problem if the
234 # git config key "diff.external" is used).
235 env = os.environ.copy()
236 if 'GIT_EXTERNAL_DIFF' in env:
237 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000238 # 'cat' is a magical git string that disables pagers on all platforms.
239 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000240
241 if find_copies:
242 similarity_options = ['--find-copies-harder', '-l100000',
243 '-C%s' % similarity]
244 else:
245 similarity_options = ['-M%s' % similarity]
246
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000247 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000248 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000249 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000251
252
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000253class Settings(object):
254 def __init__(self):
255 self.default_server = None
256 self.cc = None
257 self.root = None
258 self.is_git_svn = None
259 self.svn_branch = None
260 self.tree_status_url = None
261 self.viewvc_url = None
262 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000263 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000264 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000265
266 def LazyUpdateIfNeeded(self):
267 """Updates the settings from a codereview.settings file, if available."""
268 if not self.updated:
269 cr_settings_file = FindCodereviewSettingsFile()
270 if cr_settings_file:
271 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000272 self.updated = True
273 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000274 self.updated = True
275
276 def GetDefaultServerUrl(self, error_ok=False):
277 if not self.default_server:
278 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000279 self.default_server = gclient_utils.UpgradeToHttps(
280 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000281 if error_ok:
282 return self.default_server
283 if not self.default_server:
284 error_message = ('Could not find settings file. You must configure '
285 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000286 self.default_server = gclient_utils.UpgradeToHttps(
287 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288 return self.default_server
289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 def GetRoot(self):
291 if not self.root:
292 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
293 return self.root
294
295 def GetIsGitSvn(self):
296 """Return true if this repo looks like it's using git-svn."""
297 if self.is_git_svn is None:
298 # If you have any "svn-remote.*" config keys, we think you're using svn.
299 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000300 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000301 return self.is_git_svn
302
303 def GetSVNBranch(self):
304 if self.svn_branch is None:
305 if not self.GetIsGitSvn():
306 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
307
308 # Try to figure out which remote branch we're based on.
309 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000310 # 1) iterate through our branch history and find the svn URL.
311 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000312
313 # regexp matching the git-svn line that contains the URL.
314 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
315
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000316 env = os.environ.copy()
317 # 'cat' is a magical git string that disables pagers on all platforms.
318 env['GIT_PAGER'] = 'cat'
319
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000320 # We don't want to go through all of history, so read a line from the
321 # pipe at a time.
322 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000323 cmd = ['git', 'log', '-100', '--pretty=medium']
324 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000325 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000326 for line in proc.stdout:
327 match = git_svn_re.match(line)
328 if match:
329 url = match.group(1)
330 proc.stdout.close() # Cut pipe.
331 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000332
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000333 if url:
334 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
335 remotes = RunGit(['config', '--get-regexp',
336 r'^svn-remote\..*\.url']).splitlines()
337 for remote in remotes:
338 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000339 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000340 remote = match.group(1)
341 base_url = match.group(2)
342 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000343 ['config', 'svn-remote.%s.fetch' % remote],
344 error_ok=True).strip()
345 if fetch_spec:
346 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
347 if self.svn_branch:
348 break
349 branch_spec = RunGit(
350 ['config', 'svn-remote.%s.branches' % remote],
351 error_ok=True).strip()
352 if branch_spec:
353 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
354 if self.svn_branch:
355 break
356 tag_spec = RunGit(
357 ['config', 'svn-remote.%s.tags' % remote],
358 error_ok=True).strip()
359 if tag_spec:
360 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
361 if self.svn_branch:
362 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000363
364 if not self.svn_branch:
365 DieWithError('Can\'t guess svn branch -- try specifying it on the '
366 'command line')
367
368 return self.svn_branch
369
370 def GetTreeStatusUrl(self, error_ok=False):
371 if not self.tree_status_url:
372 error_message = ('You must configure your tree status URL by running '
373 '"git cl config".')
374 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
375 error_ok=error_ok,
376 error_message=error_message)
377 return self.tree_status_url
378
379 def GetViewVCUrl(self):
380 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000381 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000382 return self.viewvc_url
383
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000384 def GetDefaultCCList(self):
385 return self._GetConfig('rietveld.cc', error_ok=True)
386
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000387 def GetDefaultPrivateFlag(self):
388 return self._GetConfig('rietveld.private', error_ok=True)
389
ukai@chromium.orge8077812012-02-03 03:41:46 +0000390 def GetIsGerrit(self):
391 """Return true if this repo is assosiated with gerrit code review system."""
392 if self.is_gerrit is None:
393 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
394 return self.is_gerrit
395
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000396 def GetGitEditor(self):
397 """Return the editor specified in the git config, or None if none is."""
398 if self.git_editor is None:
399 self.git_editor = self._GetConfig('core.editor', error_ok=True)
400 return self.git_editor or None
401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 def _GetConfig(self, param, **kwargs):
403 self.LazyUpdateIfNeeded()
404 return RunGit(['config', param], **kwargs).strip()
405
406
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000407def ShortBranchName(branch):
408 """Convert a name like 'refs/heads/foo' to just 'foo'."""
409 return branch.replace('refs/heads/', '')
410
411
412class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000413 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000415 global settings
416 if not settings:
417 # Happens when git_cl.py is used as a utility library.
418 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 settings.GetDefaultServerUrl()
420 self.branchref = branchref
421 if self.branchref:
422 self.branch = ShortBranchName(self.branchref)
423 else:
424 self.branch = None
425 self.rietveld_server = None
426 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000427 self.lookedup_issue = False
428 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000429 self.has_description = False
430 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000431 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000432 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000433 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000434 self.cc = None
435 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000436 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000437 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000438
439 def GetCCList(self):
440 """Return the users cc'd on this CL.
441
442 Return is a string suitable for passing to gcl with the --cc flag.
443 """
444 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000445 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000446 more_cc = ','.join(self.watchers)
447 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
448 return self.cc
449
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000450 def GetCCListWithoutDefault(self):
451 """Return the users cc'd on this CL excluding default ones."""
452 if self.cc is None:
453 self.cc = ','.join(self.watchers)
454 return self.cc
455
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000456 def SetWatchers(self, watchers):
457 """Set the list of email addresses that should be cc'd based on the changed
458 files in this CL.
459 """
460 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000461
462 def GetBranch(self):
463 """Returns the short branch name, e.g. 'master'."""
464 if not self.branch:
465 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
466 self.branch = ShortBranchName(self.branchref)
467 return self.branch
468
469 def GetBranchRef(self):
470 """Returns the full branch name, e.g. 'refs/heads/master'."""
471 self.GetBranch() # Poke the lazy loader.
472 return self.branchref
473
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000474 @staticmethod
475 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 """Returns a tuple containg remote and remote ref,
477 e.g. 'origin', 'refs/heads/master'
478 """
479 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000480 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
481 error_ok=True).strip()
482 if upstream_branch:
483 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
484 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000485 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
486 error_ok=True).strip()
487 if upstream_branch:
488 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000489 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000490 # Fall back on trying a git-svn upstream branch.
491 if settings.GetIsGitSvn():
492 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000494 # Else, try to guess the origin remote.
495 remote_branches = RunGit(['branch', '-r']).split()
496 if 'origin/master' in remote_branches:
497 # Fall back on origin/master if it exits.
498 remote = 'origin'
499 upstream_branch = 'refs/heads/master'
500 elif 'origin/trunk' in remote_branches:
501 # Fall back on origin/trunk if it exists. Generally a shared
502 # git-svn clone
503 remote = 'origin'
504 upstream_branch = 'refs/heads/trunk'
505 else:
506 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507Either pass complete "git diff"-style arguments, like
508 git cl upload origin/master
509or verify this branch is set up to track another (via the --track argument to
510"git checkout -b ...").""")
511
512 return remote, upstream_branch
513
514 def GetUpstreamBranch(self):
515 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000517 if remote is not '.':
518 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
519 self.upstream_branch = upstream_branch
520 return self.upstream_branch
521
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000522 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000523 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000524 remote, branch = None, self.GetBranch()
525 seen_branches = set()
526 while branch not in seen_branches:
527 seen_branches.add(branch)
528 remote, branch = self.FetchUpstreamTuple(branch)
529 branch = ShortBranchName(branch)
530 if remote != '.' or branch.startswith('refs/remotes'):
531 break
532 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000533 remotes = RunGit(['remote'], error_ok=True).split()
534 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000535 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000536 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000537 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000538 logging.warning('Could not determine which remote this change is '
539 'associated with, so defaulting to "%s". This may '
540 'not be what you want. You may prevent this message '
541 'by running "git svn info" as documented here: %s',
542 self._remote,
543 GIT_INSTRUCTIONS_URL)
544 else:
545 logging.warn('Could not determine which remote this change is '
546 'associated with. You may prevent this message by '
547 'running "git svn info" as documented here: %s',
548 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000549 branch = 'HEAD'
550 if branch.startswith('refs/remotes'):
551 self._remote = (remote, branch)
552 else:
553 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000554 return self._remote
555
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000556 def GitSanityChecks(self, upstream_git_obj):
557 """Checks git repo status and ensures diff is from local commits."""
558
559 # Verify the commit we're diffing against is in our current branch.
560 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
561 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
562 if upstream_sha != common_ancestor:
563 print >> sys.stderr, (
564 'ERROR: %s is not in the current branch. You may need to rebase '
565 'your tracking branch' % upstream_sha)
566 return False
567
568 # List the commits inside the diff, and verify they are all local.
569 commits_in_diff = RunGit(
570 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
571 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
572 remote_branch = remote_branch.strip()
573 if code != 0:
574 _, remote_branch = self.GetRemoteBranch()
575
576 commits_in_remote = RunGit(
577 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
578
579 common_commits = set(commits_in_diff) & set(commits_in_remote)
580 if common_commits:
581 print >> sys.stderr, (
582 'ERROR: Your diff contains %d commits already in %s.\n'
583 'Run "git log --oneline %s..HEAD" to get a list of commits in '
584 'the diff. If you are using a custom git flow, you can override'
585 ' the reference used for this check with "git config '
586 'gitcl.remotebranch <git-ref>".' % (
587 len(common_commits), remote_branch, upstream_git_obj))
588 return False
589 return True
590
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000591 def GetGitBaseUrlFromConfig(self):
592 """Return the configured base URL from branch.<branchname>.baseurl.
593
594 Returns None if it is not set.
595 """
596 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
597 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599 def GetRemoteUrl(self):
600 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
601
602 Returns None if there is no remote.
603 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000604 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
606
607 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000608 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000609 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000611 self.issue = int(issue) or None if issue else None
612 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 return self.issue
614
615 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000616 if not self.rietveld_server:
617 # If we're on a branch then get the server potentially associated
618 # with that branch.
619 if self.GetIssue():
620 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
621 ['config', self._RietveldServer()], error_ok=True).strip())
622 if not self.rietveld_server:
623 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624 return self.rietveld_server
625
626 def GetIssueURL(self):
627 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000628 if not self.GetIssue():
629 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
631
632 def GetDescription(self, pretty=False):
633 if not self.has_description:
634 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000635 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000636 try:
637 self.description = self.RpcServer().get_description(issue).strip()
638 except urllib2.HTTPError, e:
639 if e.code == 404:
640 DieWithError(
641 ('\nWhile fetching the description for issue %d, received a '
642 '404 (not found)\n'
643 'error. It is likely that you deleted this '
644 'issue on the server. If this is the\n'
645 'case, please run\n\n'
646 ' git cl issue 0\n\n'
647 'to clear the association with the deleted issue. Then run '
648 'this command again.') % issue)
649 else:
650 DieWithError(
651 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 self.has_description = True
653 if pretty:
654 wrapper = textwrap.TextWrapper()
655 wrapper.initial_indent = wrapper.subsequent_indent = ' '
656 return wrapper.fill(self.description)
657 return self.description
658
659 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000660 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000661 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662 patchset = RunGit(['config', self._PatchsetSetting()],
663 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000664 self.patchset = int(patchset) or None if patchset else None
665 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666 return self.patchset
667
668 def SetPatchset(self, patchset):
669 """Set this branch's patchset. If patchset=0, clears the patchset."""
670 if patchset:
671 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673 else:
674 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000675 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000676 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000678 def GetMostRecentPatchset(self):
679 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000680
681 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000682 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000683 '/download/issue%s_%s.diff' % (issue, patchset))
684
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000685 def GetIssueProperties(self):
686 if self._props is None:
687 issue = self.GetIssue()
688 if not issue:
689 self._props = {}
690 else:
691 self._props = self.RpcServer().get_issue_properties(issue, True)
692 return self._props
693
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000694 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000695 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 def SetIssue(self, issue):
698 """Set this branch's issue. If issue=0, clears the issue."""
699 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000700 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000701 RunGit(['config', self._IssueSetting(), str(issue)])
702 if self.rietveld_server:
703 RunGit(['config', self._RietveldServer(), self.rietveld_server])
704 else:
705 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000706 self.issue = None
707 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000709 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000710 if not self.GitSanityChecks(upstream_branch):
711 DieWithError('\nGit sanity check failure')
712
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000713 env = os.environ.copy()
714 # 'cat' is a magical git string that disables pagers on all platforms.
715 env['GIT_PAGER'] = 'cat'
716
717 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000718 if not root:
719 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000720 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000721
722 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000723 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000724 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000725 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000726 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000727 except subprocess2.CalledProcessError:
728 DieWithError(
729 ('\nFailed to diff against upstream branch %s!\n\n'
730 'This branch probably doesn\'t exist anymore. To reset the\n'
731 'tracking branch, please run\n'
732 ' git branch --set-upstream %s trunk\n'
733 'replacing trunk with origin/master or the relevant branch') %
734 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000735
maruel@chromium.org52424302012-08-29 15:14:30 +0000736 issue = self.GetIssue()
737 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000738 if issue:
739 description = self.GetDescription()
740 else:
741 # If the change was never uploaded, use the log messages of all commits
742 # up to the branch point, as git cl upload will prefill the description
743 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000744 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000745 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000746 '%s...' % (upstream_branch)],
747 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000748
749 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000750 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000751 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000752 name,
753 description,
754 absroot,
755 files,
756 issue,
757 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000758 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000759
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000760 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000761 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000762
763 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000764 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000765 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000766 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000767 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000768 except presubmit_support.PresubmitFailure, e:
769 DieWithError(
770 ('%s\nMaybe your depot_tools is out of date?\n'
771 'If all fails, contact maruel@') % e)
772
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000773 def UpdateDescription(self, description):
774 self.description = description
775 return self.RpcServer().update_description(
776 self.GetIssue(), self.description)
777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000779 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000780 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000782 def SetFlag(self, flag, value):
783 """Patchset must match."""
784 if not self.GetPatchset():
785 DieWithError('The patchset needs to match. Send another patchset.')
786 try:
787 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000788 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000789 except urllib2.HTTPError, e:
790 if e.code == 404:
791 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
792 if e.code == 403:
793 DieWithError(
794 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
795 'match?') % (self.GetIssue(), self.GetPatchset()))
796 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000798 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799 """Returns an upload.RpcServer() to access this review's rietveld instance.
800 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000801 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000802 self._rpc_server = rietveld.CachingRietveld(
803 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000804 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805
806 def _IssueSetting(self):
807 """Return the git setting that stores this change's issue."""
808 return 'branch.%s.rietveldissue' % self.GetBranch()
809
810 def _PatchsetSetting(self):
811 """Return the git setting that stores this change's most recent patchset."""
812 return 'branch.%s.rietveldpatchset' % self.GetBranch()
813
814 def _RietveldServer(self):
815 """Returns the git setting that stores this change's rietveld server."""
816 return 'branch.%s.rietveldserver' % self.GetBranch()
817
818
819def GetCodereviewSettingsInteractively():
820 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000821 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 server = settings.GetDefaultServerUrl(error_ok=True)
823 prompt = 'Rietveld server (host[:port])'
824 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000825 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 if not server and not newserver:
827 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000828 if newserver:
829 newserver = gclient_utils.UpgradeToHttps(newserver)
830 if newserver != server:
831 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000833 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 prompt = caption
835 if initial:
836 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000837 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 if new_val == 'x':
839 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000840 elif new_val:
841 if is_url:
842 new_val = gclient_utils.UpgradeToHttps(new_val)
843 if new_val != initial:
844 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000846 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000847 SetProperty(settings.GetDefaultPrivateFlag(),
848 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000850 'tree-status-url', False)
851 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
853 # TODO: configure a default branch to diff against, rather than this
854 # svn-based hackery.
855
856
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000857class ChangeDescription(object):
858 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000859 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000860 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000861
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000862 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000863 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000864
agable@chromium.org42c20792013-09-12 17:34:49 +0000865 @property # www.logilab.org/ticket/89786
866 def description(self): # pylint: disable=E0202
867 return '\n'.join(self._description_lines)
868
869 def set_description(self, desc):
870 if isinstance(desc, basestring):
871 lines = desc.splitlines()
872 else:
873 lines = [line.rstrip() for line in desc]
874 while lines and not lines[0]:
875 lines.pop(0)
876 while lines and not lines[-1]:
877 lines.pop(-1)
878 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000879
880 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000881 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882 assert isinstance(reviewers, list), reviewers
883 if not reviewers:
884 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000885 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000886
agable@chromium.org42c20792013-09-12 17:34:49 +0000887 # Get the set of R= and TBR= lines and remove them from the desciption.
888 regexp = re.compile(self.R_LINE)
889 matches = [regexp.match(line) for line in self._description_lines]
890 new_desc = [l for i, l in enumerate(self._description_lines)
891 if not matches[i]]
892 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000893
agable@chromium.org42c20792013-09-12 17:34:49 +0000894 # Construct new unified R= and TBR= lines.
895 r_names = []
896 tbr_names = []
897 for match in matches:
898 if not match:
899 continue
900 people = cleanup_list([match.group(2).strip()])
901 if match.group(1) == 'TBR':
902 tbr_names.extend(people)
903 else:
904 r_names.extend(people)
905 for name in r_names:
906 if name not in reviewers:
907 reviewers.append(name)
908 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
909 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
910
911 # Put the new lines in the description where the old first R= line was.
912 line_loc = next((i for i, match in enumerate(matches) if match), -1)
913 if 0 <= line_loc < len(self._description_lines):
914 if new_tbr_line:
915 self._description_lines.insert(line_loc, new_tbr_line)
916 if new_r_line:
917 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000918 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000919 if new_r_line:
920 self.append_footer(new_r_line)
921 if new_tbr_line:
922 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000923
924 def prompt(self):
925 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000926 self.set_description([
927 '# Enter a description of the change.',
928 '# This will be displayed on the codereview site.',
929 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000930 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000931 '--------------------',
932 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000933
agable@chromium.org42c20792013-09-12 17:34:49 +0000934 regexp = re.compile(self.BUG_LINE)
935 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000936 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000937 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000938 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000939 if not content:
940 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000941 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000942
943 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000944 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
945 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000946 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000948
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000949 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000950 if self._description_lines:
951 # Add an empty line if either the last line or the new line isn't a tag.
952 last_line = self._description_lines[-1]
953 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
954 not presubmit_support.Change.TAG_LINE_RE.match(line)):
955 self._description_lines.append('')
956 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000957
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000958 def get_reviewers(self):
959 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000960 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
961 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000962 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000963
964
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000965def get_approving_reviewers(props):
966 """Retrieves the reviewers that approved a CL from the issue properties with
967 messages.
968
969 Note that the list may contain reviewers that are not committer, thus are not
970 considered by the CQ.
971 """
972 return sorted(
973 set(
974 message['sender']
975 for message in props['messages']
976 if message['approval'] and message['sender'] in props['reviewers']
977 )
978 )
979
980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981def FindCodereviewSettingsFile(filename='codereview.settings'):
982 """Finds the given file starting in the cwd and going up.
983
984 Only looks up to the top of the repository unless an
985 'inherit-review-settings-ok' file exists in the root of the repository.
986 """
987 inherit_ok_file = 'inherit-review-settings-ok'
988 cwd = os.getcwd()
989 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
990 if os.path.isfile(os.path.join(root, inherit_ok_file)):
991 root = '/'
992 while True:
993 if filename in os.listdir(cwd):
994 if os.path.isfile(os.path.join(cwd, filename)):
995 return open(os.path.join(cwd, filename))
996 if cwd == root:
997 break
998 cwd = os.path.dirname(cwd)
999
1000
1001def LoadCodereviewSettingsFromFile(fileobj):
1002 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001003 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 def SetProperty(name, setting, unset_error_ok=False):
1006 fullname = 'rietveld.' + name
1007 if setting in keyvals:
1008 RunGit(['config', fullname, keyvals[setting]])
1009 else:
1010 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1011
1012 SetProperty('server', 'CODE_REVIEW_SERVER')
1013 # Only server setting is required. Other settings can be absent.
1014 # In that case, we ignore errors raised during option deletion attempt.
1015 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001016 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1018 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1019
ukai@chromium.orge8077812012-02-03 03:41:46 +00001020 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
1021 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
1022 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001023
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1025 #should be of the form
1026 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1027 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1028 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1029 keyvals['ORIGIN_URL_CONFIG']])
1030
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001032def urlretrieve(source, destination):
1033 """urllib is broken for SSL connections via a proxy therefore we
1034 can't use urllib.urlretrieve()."""
1035 with open(destination, 'w') as f:
1036 f.write(urllib2.urlopen(source).read())
1037
1038
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001039def DownloadHooks(force):
1040 """downloads hooks
1041
1042 Args:
1043 force: True to update hooks. False to install hooks if not present.
1044 """
1045 if not settings.GetIsGerrit():
1046 return
1047 server_url = settings.GetDefaultServerUrl()
1048 src = '%s/tools/hooks/commit-msg' % server_url
1049 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1050 if not os.access(dst, os.X_OK):
1051 if os.path.exists(dst):
1052 if not force:
1053 return
1054 os.remove(dst)
1055 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001056 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001057 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1058 except Exception:
1059 if os.path.exists(dst):
1060 os.remove(dst)
1061 DieWithError('\nFailed to download hooks from %s' % src)
1062
1063
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001064@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001066 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001068 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069 if len(args) == 0:
1070 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001071 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 return 0
1073
1074 url = args[0]
1075 if not url.endswith('codereview.settings'):
1076 url = os.path.join(url, 'codereview.settings')
1077
1078 # Load code review settings and download hooks (if available).
1079 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001080 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 return 0
1082
1083
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001084def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001085 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001086 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1087 branch = ShortBranchName(branchref)
1088 _, args = parser.parse_args(args)
1089 if not args:
1090 print("Current base-url:")
1091 return RunGit(['config', 'branch.%s.base-url' % branch],
1092 error_ok=False).strip()
1093 else:
1094 print("Setting base-url to %s" % args[0])
1095 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1096 error_ok=False).strip()
1097
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001100 """Show status of changelists.
1101
1102 Colors are used to tell the state of the CL unless --fast is used:
1103 - Green LGTM'ed
1104 - Blue waiting for review
1105 - Yellow waiting for you to reply to review
1106 - Red not sent for review or broken
1107 - Cyan was committed, branch can be deleted
1108
1109 Also see 'git cl comments'.
1110 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 parser.add_option('--field',
1112 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001113 parser.add_option('-f', '--fast', action='store_true',
1114 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001116 if args:
1117 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001120 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121 if options.field.startswith('desc'):
1122 print cl.GetDescription()
1123 elif options.field == 'id':
1124 issueid = cl.GetIssue()
1125 if issueid:
1126 print issueid
1127 elif options.field == 'patch':
1128 patchset = cl.GetPatchset()
1129 if patchset:
1130 print patchset
1131 elif options.field == 'url':
1132 url = cl.GetIssueURL()
1133 if url:
1134 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001135 return 0
1136
1137 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1138 if not branches:
1139 print('No local branch found.')
1140 return 0
1141
1142 changes = (Changelist(branchref=b) for b in branches.splitlines())
1143 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1144 alignment = max(5, max(len(b) for b in branches))
1145 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001146 # Adhoc thread pool to request data concurrently.
1147 output = Queue.Queue()
1148
1149 # Silence upload.py otherwise it becomes unweldly.
1150 upload.verbosity = 0
1151
1152 if not options.fast:
1153 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001154 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001155 c = Changelist(branchref=b)
1156 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001157 props = {}
1158 r = None
1159 if i:
1160 try:
1161 props = c.GetIssueProperties()
1162 r = c.GetApprovingReviewers() if i else None
1163 except urllib2.HTTPError:
1164 # The issue probably doesn't exist anymore.
1165 i += ' (broken)'
1166
1167 msgs = props.get('messages') or []
1168
1169 if not i:
1170 color = Fore.WHITE
1171 elif props.get('closed'):
1172 # Issue is closed.
1173 color = Fore.CYAN
1174 elif r:
1175 # Was LGTM'ed.
1176 color = Fore.GREEN
1177 elif not msgs:
1178 # No message was sent.
1179 color = Fore.RED
1180 elif msgs[-1]['sender'] != props.get('owner_email'):
1181 color = Fore.YELLOW
1182 else:
1183 color = Fore.BLUE
1184 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001185
1186 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1187 for t in threads:
1188 t.daemon = True
1189 t.start()
1190 else:
1191 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1192 for b in branches:
1193 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001194 url = c.GetIssueURL()
1195 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001196
1197 tmp = {}
1198 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001199 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001200 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001201 b, i, color = output.get()
1202 tmp[b] = (i, color)
1203 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001204 reset = Fore.RESET
1205 if not sys.stdout.isatty():
1206 color = ''
1207 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001209 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001210
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001211 cl = Changelist()
1212 print
1213 print 'Current branch:',
1214 if not cl.GetIssue():
1215 print 'no issue assigned.'
1216 return 0
1217 print cl.GetBranch()
1218 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1219 print 'Issue description:'
1220 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 return 0
1222
1223
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001224def colorize_CMDstatus_doc():
1225 """To be called once in main() to add colors to git cl status help."""
1226 colors = [i for i in dir(Fore) if i[0].isupper()]
1227
1228 def colorize_line(line):
1229 for color in colors:
1230 if color in line.upper():
1231 # Extract whitespaces first and the leading '-'.
1232 indent = len(line) - len(line.lstrip(' ')) + 1
1233 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1234 return line
1235
1236 lines = CMDstatus.__doc__.splitlines()
1237 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1238
1239
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001240@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001242 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243
1244 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001245 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001246 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247
1248 cl = Changelist()
1249 if len(args) > 0:
1250 try:
1251 issue = int(args[0])
1252 except ValueError:
1253 DieWithError('Pass a number to set the issue or none to list it.\n'
1254 'Maybe you want to run git cl status?')
1255 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001256 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 return 0
1258
1259
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001260def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001261 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001262 (_, args) = parser.parse_args(args)
1263 if args:
1264 parser.error('Unsupported argument: %s' % args)
1265
1266 cl = Changelist()
1267 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001268 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001269 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001270 if message['disapproval']:
1271 color = Fore.RED
1272 elif message['approval']:
1273 color = Fore.GREEN
1274 elif message['sender'] == data['owner_email']:
1275 color = Fore.MAGENTA
1276 else:
1277 color = Fore.BLUE
1278 print '\n%s%s %s%s' % (
1279 color, message['date'].split('.', 1)[0], message['sender'],
1280 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001281 if message['text'].strip():
1282 print '\n'.join(' ' + l for l in message['text'].splitlines())
1283 return 0
1284
1285
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001286def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001287 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001288 cl = Changelist()
1289 if not cl.GetIssue():
1290 DieWithError('This branch has no associated changelist.')
1291 description = ChangeDescription(cl.GetDescription())
1292 description.prompt()
1293 cl.UpdateDescription(description.description)
1294 return 0
1295
1296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297def CreateDescriptionFromLog(args):
1298 """Pulls out the commit log to use as a base for the CL description."""
1299 log_args = []
1300 if len(args) == 1 and not args[0].endswith('.'):
1301 log_args = [args[0] + '..']
1302 elif len(args) == 1 and args[0].endswith('...'):
1303 log_args = [args[0][:-1]]
1304 elif len(args) == 2:
1305 log_args = [args[0] + '..' + args[1]]
1306 else:
1307 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001308 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309
1310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001312 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001313 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001315 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001316 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 (options, args) = parser.parse_args(args)
1318
ukai@chromium.org259e4682012-10-25 07:36:33 +00001319 if not options.force and is_dirty_git_tree('presubmit'):
1320 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 return 1
1322
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001323 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324 if args:
1325 base_branch = args[0]
1326 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 # Default to diffing against the common ancestor of the upstream branch.
1328 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001330 cl.RunHook(
1331 committing=not options.upload,
1332 may_prompt=False,
1333 verbose=options.verbose,
1334 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001335 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336
1337
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001338def AddChangeIdToCommitMessage(options, args):
1339 """Re-commits using the current message, assumes the commit hook is in
1340 place.
1341 """
1342 log_desc = options.message or CreateDescriptionFromLog(args)
1343 git_command = ['commit', '--amend', '-m', log_desc]
1344 RunGit(git_command)
1345 new_log_desc = CreateDescriptionFromLog(args)
1346 if CHANGE_ID in new_log_desc:
1347 print 'git-cl: Added Change-Id to commit message.'
1348 else:
1349 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1350
1351
ukai@chromium.orge8077812012-02-03 03:41:46 +00001352def GerritUpload(options, args, cl):
1353 """upload the current branch to gerrit."""
1354 # We assume the remote called "origin" is the one we want.
1355 # It is probably not worthwhile to support different workflows.
1356 remote = 'origin'
1357 branch = 'master'
1358 if options.target_branch:
1359 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001361 change_desc = ChangeDescription(
1362 options.message or CreateDescriptionFromLog(args))
1363 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001364 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001366 if CHANGE_ID not in change_desc.description:
1367 AddChangeIdToCommitMessage(options, args)
1368 if options.reviewers:
1369 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
ukai@chromium.orge8077812012-02-03 03:41:46 +00001371 receive_options = []
1372 cc = cl.GetCCList().split(',')
1373 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001374 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001375 cc = filter(None, cc)
1376 if cc:
1377 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001378 if change_desc.get_reviewers():
1379 receive_options.extend(
1380 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
ukai@chromium.orge8077812012-02-03 03:41:46 +00001382 git_command = ['push']
1383 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001384 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385 ' '.join(receive_options))
1386 git_command += [remote, 'HEAD:refs/for/' + branch]
1387 RunGit(git_command)
1388 # TODO(ukai): parse Change-Id: and set issue number?
1389 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001391
ukai@chromium.orge8077812012-02-03 03:41:46 +00001392def RietveldUpload(options, args, cl):
1393 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1395 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 if options.emulate_svn_auto_props:
1397 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398
1399 change_desc = None
1400
1401 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001402 if options.title:
1403 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001404 if options.message:
1405 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001406 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 print ("This branch is associated with issue %s. "
1408 "Adding patch to that issue." % cl.GetIssue())
1409 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001410 if options.title:
1411 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001412 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001413 change_desc = ChangeDescription(message)
1414 if options.reviewers:
1415 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001416 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001417 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001418
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001419 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 print "Description is empty; aborting."
1421 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001422
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001423 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001424 if change_desc.get_reviewers():
1425 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001426 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001427 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001428 DieWithError("Must specify reviewers to send email.")
1429 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001430
1431 # We check this before applying rietveld.private assuming that in
1432 # rietveld.cc only addresses which we can send private CLs to are listed
1433 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1434 # --private is specified explicitly on the command line.
1435 if options.private:
1436 logging.warn('rietveld.cc is ignored since private flag is specified. '
1437 'You need to review and add them manually if necessary.')
1438 cc = cl.GetCCListWithoutDefault()
1439 else:
1440 cc = cl.GetCCList()
1441 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001442 if cc:
1443 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001445 if options.private or settings.GetDefaultPrivateFlag() == "True":
1446 upload_args.append('--private')
1447
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001448 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001449 if not options.find_copies:
1450 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452 # Include the upstream repo's URL in the change -- this is useful for
1453 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001454 remote_url = cl.GetGitBaseUrlFromConfig()
1455 if not remote_url:
1456 if settings.GetIsGitSvn():
1457 # URL is dependent on the current directory.
1458 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1459 if data:
1460 keys = dict(line.split(': ', 1) for line in data.splitlines()
1461 if ': ' in line)
1462 remote_url = keys.get('URL', None)
1463 else:
1464 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1465 remote_url = (cl.GetRemoteUrl() + '@'
1466 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 if remote_url:
1468 upload_args.extend(['--base_url', remote_url])
1469
1470 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001471 upload_args = ['upload'] + upload_args + args
1472 logging.info('upload.RealMain(%s)', upload_args)
1473 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001474 issue = int(issue)
1475 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001476 except KeyboardInterrupt:
1477 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 except:
1479 # If we got an exception after the user typed a description for their
1480 # change, back up the description before re-raising.
1481 if change_desc:
1482 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1483 print '\nGot exception while uploading -- saving description to %s\n' \
1484 % backup_path
1485 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001486 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 backup_file.close()
1488 raise
1489
1490 if not cl.GetIssue():
1491 cl.SetIssue(issue)
1492 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001493
1494 if options.use_commit_queue:
1495 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496 return 0
1497
1498
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001499def cleanup_list(l):
1500 """Fixes a list so that comma separated items are put as individual items.
1501
1502 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1503 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1504 """
1505 items = sum((i.split(',') for i in l), [])
1506 stripped_items = (i.strip() for i in items)
1507 return sorted(filter(None, stripped_items))
1508
1509
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001510@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001511def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001512 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001513 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1514 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001515 parser.add_option('--bypass-watchlists', action='store_true',
1516 dest='bypass_watchlists',
1517 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001518 parser.add_option('-f', action='store_true', dest='force',
1519 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001520 parser.add_option('-m', dest='message', help='message for patchset')
1521 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001522 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001523 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001524 help='reviewer email addresses')
1525 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001526 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001527 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001528 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001529 help='send email to reviewer immediately')
1530 parser.add_option("--emulate_svn_auto_props", action="store_true",
1531 dest="emulate_svn_auto_props",
1532 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001533 parser.add_option('-c', '--use-commit-queue', action='store_true',
1534 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001535 parser.add_option('--private', action='store_true',
1536 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001537 parser.add_option('--target_branch',
1538 help='When uploading to gerrit, remote branch to '
1539 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001540 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541 (options, args) = parser.parse_args(args)
1542
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001543 if options.target_branch and not settings.GetIsGerrit():
1544 parser.error('Use --target_branch for non gerrit repository.')
1545
ukai@chromium.org259e4682012-10-25 07:36:33 +00001546 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547 return 1
1548
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001549 options.reviewers = cleanup_list(options.reviewers)
1550 options.cc = cleanup_list(options.cc)
1551
ukai@chromium.orge8077812012-02-03 03:41:46 +00001552 cl = Changelist()
1553 if args:
1554 # TODO(ukai): is it ok for gerrit case?
1555 base_branch = args[0]
1556 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001557 # Default to diffing against common ancestor of upstream branch
1558 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001559 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001560
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001561 # Apply watchlists on upload.
1562 change = cl.GetChange(base_branch, None)
1563 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1564 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001565 if not options.bypass_watchlists:
1566 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001567
ukai@chromium.orge8077812012-02-03 03:41:46 +00001568 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001569 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001570 may_prompt=not options.force,
1571 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001572 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573 if not hook_results.should_continue():
1574 return 1
1575 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001576 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001577
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001578 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001579 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001580 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001581 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001582 print ('The last upload made from this repository was patchset #%d but '
1583 'the most recent patchset on the server is #%d.'
1584 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001585 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1586 'from another machine or branch the patch you\'re uploading now '
1587 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001588 ask_for_data('About to upload; enter to confirm.')
1589
iannucci@chromium.org79540052012-10-19 23:15:26 +00001590 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001591 if settings.GetIsGerrit():
1592 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001593 ret = RietveldUpload(options, args, cl)
1594 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001595 git_set_branch_value('last-upload-hash',
1596 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001597
1598 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001599
1600
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001601def IsSubmoduleMergeCommit(ref):
1602 # When submodules are added to the repo, we expect there to be a single
1603 # non-git-svn merge commit at remote HEAD with a signature comment.
1604 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001605 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001606 return RunGit(cmd) != ''
1607
1608
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609def SendUpstream(parser, args, cmd):
1610 """Common code for CmdPush and CmdDCommit
1611
1612 Squashed commit into a single.
1613 Updates changelog with metadata (e.g. pointer to review).
1614 Pushes/dcommits the code upstream.
1615 Updates review and closes.
1616 """
1617 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1618 help='bypass upload presubmit hook')
1619 parser.add_option('-m', dest='message',
1620 help="override review description")
1621 parser.add_option('-f', action='store_true', dest='force',
1622 help="force yes to questions (don't prompt)")
1623 parser.add_option('-c', dest='contributor',
1624 help="external contributor for patch (appended to " +
1625 "description and used as author for git). Should be " +
1626 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001627 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628 (options, args) = parser.parse_args(args)
1629 cl = Changelist()
1630
1631 if not args or cmd == 'push':
1632 # Default to merging against our best guess of the upstream branch.
1633 args = [cl.GetUpstreamBranch()]
1634
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001635 if options.contributor:
1636 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1637 print "Please provide contibutor as 'First Last <email@example.com>'"
1638 return 1
1639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001641 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642
ukai@chromium.org259e4682012-10-25 07:36:33 +00001643 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001644 return 1
1645
1646 # This rev-list syntax means "show all commits not in my branch that
1647 # are in base_branch".
1648 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1649 base_branch]).splitlines()
1650 if upstream_commits:
1651 print ('Base branch "%s" has %d commits '
1652 'not in this branch.' % (base_branch, len(upstream_commits)))
1653 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1654 return 1
1655
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001656 # This is the revision `svn dcommit` will commit on top of.
1657 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1658 '--pretty=format:%H'])
1659
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001660 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001661 # If the base_head is a submodule merge commit, the first parent of the
1662 # base_head should be a git-svn commit, which is what we're interested in.
1663 base_svn_head = base_branch
1664 if base_has_submodules:
1665 base_svn_head += '^1'
1666
1667 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001668 if extra_commits:
1669 print ('This branch has %d additional commits not upstreamed yet.'
1670 % len(extra_commits.splitlines()))
1671 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1672 'before attempting to %s.' % (base_branch, cmd))
1673 return 1
1674
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001675 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001676 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001677 author = None
1678 if options.contributor:
1679 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001680 hook_results = cl.RunHook(
1681 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001682 may_prompt=not options.force,
1683 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001684 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001685 if not hook_results.should_continue():
1686 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001687
1688 if cmd == 'dcommit':
1689 # Check the tree status if the tree status URL is set.
1690 status = GetTreeStatus()
1691 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001692 print('The tree is closed. Please wait for it to reopen. Use '
1693 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001694 return 1
1695 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001696 print('Unable to determine tree status. Please verify manually and '
1697 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001698 else:
1699 breakpad.SendStack(
1700 'GitClHooksBypassedCommit',
1701 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001702 (cl.GetRietveldServer(), cl.GetIssue()),
1703 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001705 change_desc = ChangeDescription(options.message)
1706 if not change_desc.description and cl.GetIssue():
1707 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001709 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001710 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001711 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001712 else:
1713 print 'No description set.'
1714 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1715 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001717 # Keep a separate copy for the commit message, because the commit message
1718 # contains the link to the Rietveld issue, while the Rietveld message contains
1719 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001720 # Keep a separate copy for the commit message.
1721 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001722 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001723
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001724 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001725 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001726 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001727 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001728 commit_desc.append_footer('Patch from %s.' % options.contributor)
1729
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001730 print('Description:')
1731 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732
1733 branches = [base_branch, cl.GetBranchRef()]
1734 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001735 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001736 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001738 # We want to squash all this branch's commits into one commit with the proper
1739 # description. We do this by doing a "reset --soft" to the base branch (which
1740 # keeps the working copy the same), then dcommitting that. If origin/master
1741 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1742 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001744 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1745 # Delete the branches if they exist.
1746 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1747 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1748 result = RunGitWithCode(showref_cmd)
1749 if result[0] == 0:
1750 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751
1752 # We might be in a directory that's present in this branch but not in the
1753 # trunk. Move up to the top of the tree so that git commands that expect a
1754 # valid CWD won't fail after we check out the merge branch.
1755 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1756 if rel_base_path:
1757 os.chdir(rel_base_path)
1758
1759 # Stuff our change into the merge branch.
1760 # We wrap in a try...finally block so if anything goes wrong,
1761 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001762 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001764 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1765 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001767 RunGit(
1768 [
1769 'commit', '--author', options.contributor,
1770 '-m', commit_desc.description,
1771 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001773 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001774 if base_has_submodules:
1775 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1776 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1777 RunGit(['checkout', CHERRY_PICK_BRANCH])
1778 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 if cmd == 'push':
1780 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001781 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782 retcode, output = RunGitWithCode(
1783 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1784 logging.debug(output)
1785 else:
1786 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001787 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001788 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001789 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790 finally:
1791 # And then swap back to the original branch and clean up.
1792 RunGit(['checkout', '-q', cl.GetBranch()])
1793 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001794 if base_has_submodules:
1795 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796
1797 if cl.GetIssue():
1798 if cmd == 'dcommit' and 'Committed r' in output:
1799 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1800 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001801 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1802 for l in output.splitlines(False))
1803 match = filter(None, match)
1804 if len(match) != 1:
1805 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1806 output)
1807 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808 else:
1809 return 1
1810 viewvc_url = settings.GetViewVCUrl()
1811 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001812 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001813 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001814 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 print ('Closing issue '
1816 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001817 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001819 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001820 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001821 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001822 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1823 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001824 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001825
1826 if retcode == 0:
1827 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1828 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001829 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831 return 0
1832
1833
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001834@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001836 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001837 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001838 message = """This doesn't appear to be an SVN repository.
1839If your project has a git mirror with an upstream SVN master, you probably need
1840to run 'git svn init', see your project's git mirror documentation.
1841If your project has a true writeable upstream repository, you probably want
1842to run 'git cl push' instead.
1843Choose wisely, if you get this wrong, your commit might appear to succeed but
1844will instead be silently ignored."""
1845 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001846 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847 return SendUpstream(parser, args, 'dcommit')
1848
1849
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001850@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001852 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853 if settings.GetIsGitSvn():
1854 print('This appears to be an SVN repository.')
1855 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001856 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001857 return SendUpstream(parser, args, 'push')
1858
1859
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001860@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001862 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 parser.add_option('-b', dest='newbranch',
1864 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001865 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001866 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001867 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1868 help='Change to the directory DIR immediately, '
1869 'before doing anything else.')
1870 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001871 help='failed patches spew .rej files rather than '
1872 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001873 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1874 help="don't commit after patch applies")
1875 (options, args) = parser.parse_args(args)
1876 if len(args) != 1:
1877 parser.print_help()
1878 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001879 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001881 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001882 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001883
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001884 if options.newbranch:
1885 if options.force:
1886 RunGit(['branch', '-D', options.newbranch],
1887 stderr=subprocess2.PIPE, error_ok=True)
1888 RunGit(['checkout', '-b', options.newbranch,
1889 Changelist().GetUpstreamBranch()])
1890
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001891 return PatchIssue(issue_arg, options.reject, options.nocommit,
1892 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001893
1894
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001895def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001896 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001898 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001899 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001900 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001901 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001903 # Assume it's a URL to the patch. Default to https.
1904 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001905 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001906 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907 DieWithError('Must pass an issue ID or full URL for '
1908 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001909 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001910 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001911 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 # Switch up to the top-level directory, if necessary, in preparation for
1914 # applying the patch.
1915 top = RunGit(['rev-parse', '--show-cdup']).strip()
1916 if top:
1917 os.chdir(top)
1918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919 # Git patches have a/ at the beginning of source paths. We strip that out
1920 # with a sed script rather than the -p flag to patch so we can feed either
1921 # Git or svn-style patches into the same apply command.
1922 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001923 try:
1924 patch_data = subprocess2.check_output(
1925 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1926 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 DieWithError('Git patch mungling failed.')
1928 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001929 env = os.environ.copy()
1930 # 'cat' is a magical git string that disables pagers on all platforms.
1931 env['GIT_PAGER'] = 'cat'
1932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 # We use "git apply" to apply the patch instead of "patch" so that we can
1934 # pick up file adds.
1935 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001936 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001937 if directory:
1938 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001939 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001941 elif IsGitVersionAtLeast('1.7.12'):
1942 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001943 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001944 subprocess2.check_call(cmd, env=env,
1945 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001946 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947 DieWithError('Failed to apply the patch')
1948
1949 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001950 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1952 cl = Changelist()
1953 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001954 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001955 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001956 else:
1957 print "Patch applied to index."
1958 return 0
1959
1960
1961def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001962 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 # Provide a wrapper for git svn rebase to help avoid accidental
1964 # git svn dcommit.
1965 # It's the only command that doesn't use parser at all since we just defer
1966 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001967 env = os.environ.copy()
1968 # 'cat' is a magical git string that disables pagers on all platforms.
1969 env['GIT_PAGER'] = 'cat'
1970
1971 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001972
1973
1974def GetTreeStatus():
1975 """Fetches the tree status and returns either 'open', 'closed',
1976 'unknown' or 'unset'."""
1977 url = settings.GetTreeStatusUrl(error_ok=True)
1978 if url:
1979 status = urllib2.urlopen(url).read().lower()
1980 if status.find('closed') != -1 or status == '0':
1981 return 'closed'
1982 elif status.find('open') != -1 or status == '1':
1983 return 'open'
1984 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985 return 'unset'
1986
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001987
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001988def GetTreeStatusReason():
1989 """Fetches the tree status from a json url and returns the message
1990 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001991 url = settings.GetTreeStatusUrl()
1992 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993 connection = urllib2.urlopen(json_url)
1994 status = json.loads(connection.read())
1995 connection.close()
1996 return status['message']
1997
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002000 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002001 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002 status = GetTreeStatus()
2003 if 'unset' == status:
2004 print 'You must configure your tree status URL by running "git cl config".'
2005 return 2
2006
2007 print "The tree is %s" % status
2008 print
2009 print GetTreeStatusReason()
2010 if status != 'open':
2011 return 1
2012 return 0
2013
2014
maruel@chromium.org15192402012-09-06 12:38:29 +00002015def CMDtry(parser, args):
2016 """Triggers a try job through Rietveld."""
2017 group = optparse.OptionGroup(parser, "Try job options")
2018 group.add_option(
2019 "-b", "--bot", action="append",
2020 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2021 "times to specify multiple builders. ex: "
2022 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2023 "the try server waterfall for the builders name and the tests "
2024 "available. Can also be used to specify gtest_filter, e.g. "
2025 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2026 group.add_option(
2027 "-r", "--revision",
2028 help="Revision to use for the try job; default: the "
2029 "revision will be determined by the try server; see "
2030 "its waterfall for more info")
2031 group.add_option(
2032 "-c", "--clobber", action="store_true", default=False,
2033 help="Force a clobber before building; e.g. don't do an "
2034 "incremental build")
2035 group.add_option(
2036 "--project",
2037 help="Override which project to use. Projects are defined "
2038 "server-side to define what default bot set to use")
2039 group.add_option(
2040 "-t", "--testfilter", action="append", default=[],
2041 help=("Apply a testfilter to all the selected builders. Unless the "
2042 "builders configurations are similar, use multiple "
2043 "--bot <builder>:<test> arguments."))
2044 group.add_option(
2045 "-n", "--name", help="Try job name; default to current branch name")
2046 parser.add_option_group(group)
2047 options, args = parser.parse_args(args)
2048
2049 if args:
2050 parser.error('Unknown arguments: %s' % args)
2051
2052 cl = Changelist()
2053 if not cl.GetIssue():
2054 parser.error('Need to upload first')
2055
2056 if not options.name:
2057 options.name = cl.GetBranch()
2058
2059 # Process --bot and --testfilter.
2060 if not options.bot:
2061 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002062 change = cl.GetChange(
2063 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2064 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002065 options.bot = presubmit_support.DoGetTrySlaves(
2066 change,
2067 change.LocalPaths(),
2068 settings.GetRoot(),
2069 None,
2070 None,
2071 options.verbose,
2072 sys.stdout)
2073 if not options.bot:
2074 parser.error('No default try builder to try, use --bot')
2075
2076 builders_and_tests = {}
2077 for bot in options.bot:
2078 if ':' in bot:
2079 builder, tests = bot.split(':', 1)
2080 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2081 elif ',' in bot:
2082 parser.error('Specify one bot per --bot flag')
2083 else:
2084 builders_and_tests.setdefault(bot, []).append('defaulttests')
2085
2086 if options.testfilter:
2087 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2088 builders_and_tests = dict(
2089 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2090 if t != ['compile'])
2091
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002092 if any('triggered' in b for b in builders_and_tests):
2093 print >> sys.stderr, (
2094 'ERROR You are trying to send a job to a triggered bot. This type of'
2095 ' bot requires an\ninitial job from a parent (usually a builder). '
2096 'Instead send your job to the parent.\n'
2097 'Bot list: %s' % builders_and_tests)
2098 return 1
2099
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002100 patchset = cl.GetMostRecentPatchset()
2101 if patchset and patchset != cl.GetPatchset():
2102 print(
2103 '\nWARNING Mismatch between local config and server. Did a previous '
2104 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2105 'Continuing using\npatchset %s.\n' % patchset)
maruel@chromium.org15192402012-09-06 12:38:29 +00002106
2107 cl.RpcServer().trigger_try_jobs(
2108 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2109 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002110 print('Tried jobs on:')
2111 length = max(len(builder) for builder in builders_and_tests)
2112 for builder in sorted(builders_and_tests):
2113 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002114 return 0
2115
2116
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002117@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002119 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002120 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002121 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002122 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002123 return 0
2124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002126 if args:
2127 # One arg means set upstream branch.
2128 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2129 cl = Changelist()
2130 print "Upstream branch set to " + cl.GetUpstreamBranch()
2131 else:
2132 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133 return 0
2134
2135
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002136def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002137 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002138 _, args = parser.parse_args(args)
2139 if args:
2140 parser.error('Unrecognized args: %s' % ' '.join(args))
2141 cl = Changelist()
2142 cl.SetFlag('commit', '1')
2143 return 0
2144
2145
groby@chromium.org411034a2013-02-26 15:12:01 +00002146def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002147 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002148 _, args = parser.parse_args(args)
2149 if args:
2150 parser.error('Unrecognized args: %s' % ' '.join(args))
2151 cl = Changelist()
2152 # Ensure there actually is an issue to close.
2153 cl.GetDescription()
2154 cl.CloseIssue()
2155 return 0
2156
2157
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002158def CMDdiff(parser, args):
2159 """shows differences between local tree and last upload."""
2160 cl = Changelist()
2161 branch = cl.GetBranch()
2162 TMP_BRANCH = 'git-cl-diff'
2163 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2164
2165 # Create a new branch based on the merge-base
2166 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2167 try:
2168 # Patch in the latest changes from rietveld.
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002169 rtn = PatchIssue(cl.GetIssue(), False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002170 if rtn != 0:
2171 return rtn
2172
2173 # Switch back to starting brand and diff against the temporary
2174 # branch containing the latest rietveld patch.
2175 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2176 finally:
2177 RunGit(['checkout', '-q', branch])
2178 RunGit(['branch', '-D', TMP_BRANCH])
2179
2180 return 0
2181
2182
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002183def CMDowners(parser, args):
2184 """interactively find the owners for reviewing"""
2185 parser.add_option(
2186 '--no-color',
2187 action='store_true',
2188 help='Use this option to disable color output')
2189 options, args = parser.parse_args(args)
2190
2191 author = RunGit(['config', 'user.email']).strip() or None
2192
2193 cl = Changelist()
2194
2195 if args:
2196 if len(args) > 1:
2197 parser.error('Unknown args')
2198 base_branch = args[0]
2199 else:
2200 # Default to diffing against the common ancestor of the upstream branch.
2201 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2202
2203 change = cl.GetChange(base_branch, None)
2204 return owners_finder.OwnersFinder(
2205 [f.LocalPath() for f in
2206 cl.GetChange(base_branch, None).AffectedFiles()],
2207 change.RepositoryRoot(), author,
2208 fopen=file, os_path=os.path, glob=glob.glob,
2209 disable_color=options.no_color).run()
2210
2211
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002212def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002213 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002214 CLANG_EXTS = ['.cc', '.cpp', '.h']
2215 parser.add_option('--full', action='store_true', default=False)
2216 opts, args = parser.parse_args(args)
2217 if args:
2218 parser.error('Unrecognized args: %s' % ' '.join(args))
2219
digit@chromium.org29e47272013-05-17 17:01:46 +00002220 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002221 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002222 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002223 # Only list the names of modified files.
2224 diff_cmd.append('--name-only')
2225 else:
2226 # Only generate context-less patches.
2227 diff_cmd.append('-U0')
2228
2229 # Grab the merge-base commit, i.e. the upstream commit of the current
2230 # branch when it was created or the last time it was rebased. This is
2231 # to cover the case where the user may have called "git fetch origin",
2232 # moving the origin branch to a newer commit, but hasn't rebased yet.
2233 upstream_commit = None
2234 cl = Changelist()
2235 upstream_branch = cl.GetUpstreamBranch()
2236 if upstream_branch:
2237 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2238 upstream_commit = upstream_commit.strip()
2239
2240 if not upstream_commit:
2241 DieWithError('Could not find base commit for this branch. '
2242 'Are you in detached state?')
2243
2244 diff_cmd.append(upstream_commit)
2245
2246 # Handle source file filtering.
2247 diff_cmd.append('--')
2248 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2249 diff_output = RunGit(diff_cmd)
2250
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002251 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2252
digit@chromium.org29e47272013-05-17 17:01:46 +00002253 if opts.full:
2254 # diff_output is a list of files to send to clang-format.
2255 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002256 if not files:
2257 print "Nothing to format."
2258 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002259 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2260 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002261 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002262 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002263 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2264 'clang-format-diff.py')
2265 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002266 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002267 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002268 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002269
2270 return 0
2271
2272
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002273class OptionParser(optparse.OptionParser):
2274 """Creates the option parse and add --verbose support."""
2275 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002276 optparse.OptionParser.__init__(
2277 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002278 self.add_option(
2279 '-v', '--verbose', action='count', default=0,
2280 help='Use 2 times for more debugging info')
2281
2282 def parse_args(self, args=None, values=None):
2283 options, args = optparse.OptionParser.parse_args(self, args, values)
2284 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2285 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2286 return options, args
2287
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002289def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002290 if sys.hexversion < 0x02060000:
2291 print >> sys.stderr, (
2292 '\nYour python version %s is unsupported, please upgrade.\n' %
2293 sys.version.split(' ', 1)[0])
2294 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002295
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002296 # Reload settings.
2297 global settings
2298 settings = Settings()
2299
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002300 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002301 dispatcher = subcommand.CommandDispatcher(__name__)
2302 try:
2303 return dispatcher.execute(OptionParser(), argv)
2304 except urllib2.HTTPError, e:
2305 if e.code != 500:
2306 raise
2307 DieWithError(
2308 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2309 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002310
2311
2312if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002313 # These affect sys.stdout so do it outside of main() to simplify mocks in
2314 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002315 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002316 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002317 sys.exit(main(sys.argv[1:]))