blob: e7b2fe49aa9b7ad30618503ff18fa5acaff74c68 [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
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000039import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000041import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000044import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045
maruel@chromium.org0633fb42013-08-16 20:06:14 +000046__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000048DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000049POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000051GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000052CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000054# Shortcut since it quickly becomes redundant.
55Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000056
maruel@chromium.orgddd59412011-11-30 14:20:38 +000057# Initialized in main()
58settings = None
59
60
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000062 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 sys.exit(1)
64
65
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000066def GetNoGitPagerEnv():
67 env = os.environ.copy()
68 # 'cat' is a magical git string that disables pagers on all platforms.
69 env['GIT_PAGER'] = 'cat'
70 return env
71
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000074 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000075 except subprocess2.CalledProcessError as e:
76 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079 'Command "%s" failed.\n%s' % (
80 ' '.join(args), error_message or e.stdout or ''))
81 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082
83
84def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000085 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000086 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087
88
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000089def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000090 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000091 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000092 if suppress_stderr:
93 stderr = subprocess2.VOID
94 else:
95 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +000096 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000097 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000098 stdout=subprocess2.PIPE,
99 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000100 return code, out[0]
101 except ValueError:
102 # When the subprocess fails, it returns None. That triggers a ValueError
103 # when trying to unpack the return value into (out, code).
104 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
106
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000107def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000108 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000109 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000110 return (version.startswith(prefix) and
111 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000112
113
maruel@chromium.org90541732011-04-01 17:54:18 +0000114def ask_for_data(prompt):
115 try:
116 return raw_input(prompt)
117 except KeyboardInterrupt:
118 # Hide the exception.
119 sys.exit(1)
120
121
iannucci@chromium.org79540052012-10-19 23:15:26 +0000122def git_set_branch_value(key, value):
123 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000124 if not branch:
125 return
126
127 cmd = ['config']
128 if isinstance(value, int):
129 cmd.append('--int')
130 git_key = 'branch.%s.%s' % (branch, key)
131 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000132
133
134def git_get_branch_default(key, default):
135 branch = Changelist().GetBranch()
136 if branch:
137 git_key = 'branch.%s.%s' % (branch, key)
138 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
139 try:
140 return int(stdout.strip())
141 except ValueError:
142 pass
143 return default
144
145
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000146def add_git_similarity(parser):
147 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000148 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000149 help='Sets the percentage that a pair of files need to match in order to'
150 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000151 parser.add_option(
152 '--find-copies', action='store_true',
153 help='Allows git to look for copies.')
154 parser.add_option(
155 '--no-find-copies', action='store_false', dest='find_copies',
156 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000157
158 old_parser_args = parser.parse_args
159 def Parse(args):
160 options, args = old_parser_args(args)
161
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000162 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000163 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000165 print('Note: Saving similarity of %d%% in git config.'
166 % options.similarity)
167 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000168
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169 options.similarity = max(0, min(options.similarity, 100))
170
171 if options.find_copies is None:
172 options.find_copies = bool(
173 git_get_branch_default('git-find-copies', True))
174 else:
175 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000176
177 print('Using %d%% similarity for rename/copy detection. '
178 'Override with --similarity.' % options.similarity)
179
180 return options, args
181 parser.parse_args = Parse
182
183
ukai@chromium.org259e4682012-10-25 07:36:33 +0000184def is_dirty_git_tree(cmd):
185 # Make sure index is up-to-date before running diff-index.
186 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
187 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
188 if dirty:
189 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
190 print 'Uncommitted files: (git diff-index --name-status HEAD)'
191 print dirty[:4096]
192 if len(dirty) > 4096:
193 print '... (run "git diff-index --name-status HEAD" to see full output).'
194 return True
195 return False
196
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000197
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000198def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
199 """Return the corresponding git ref if |base_url| together with |glob_spec|
200 matches the full |url|.
201
202 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
203 """
204 fetch_suburl, as_ref = glob_spec.split(':')
205 if allow_wildcards:
206 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
207 if glob_match:
208 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
209 # "branches/{472,597,648}/src:refs/remotes/svn/*".
210 branch_re = re.escape(base_url)
211 if glob_match.group(1):
212 branch_re += '/' + re.escape(glob_match.group(1))
213 wildcard = glob_match.group(2)
214 if wildcard == '*':
215 branch_re += '([^/]*)'
216 else:
217 # Escape and replace surrounding braces with parentheses and commas
218 # with pipe symbols.
219 wildcard = re.escape(wildcard)
220 wildcard = re.sub('^\\\\{', '(', wildcard)
221 wildcard = re.sub('\\\\,', '|', wildcard)
222 wildcard = re.sub('\\\\}$', ')', wildcard)
223 branch_re += wildcard
224 if glob_match.group(3):
225 branch_re += re.escape(glob_match.group(3))
226 match = re.match(branch_re, url)
227 if match:
228 return re.sub('\*$', match.group(1), as_ref)
229
230 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
231 if fetch_suburl:
232 full_url = base_url + '/' + fetch_suburl
233 else:
234 full_url = base_url
235 if full_url == url:
236 return as_ref
237 return None
238
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000239
iannucci@chromium.org79540052012-10-19 23:15:26 +0000240def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000241 """Prints statistics about the change to the user."""
242 # --no-ext-diff is broken in some versions of Git, so try to work around
243 # this by overriding the environment (but there is still a problem if the
244 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000245 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000246 if 'GIT_EXTERNAL_DIFF' in env:
247 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248
249 if find_copies:
250 similarity_options = ['--find-copies-harder', '-l100000',
251 '-C%s' % similarity]
252 else:
253 similarity_options = ['-M%s' % similarity]
254
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000255 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000256 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000257 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000258 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000259
260
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000261class Settings(object):
262 def __init__(self):
263 self.default_server = None
264 self.cc = None
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000265 self.relative_root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266 self.is_git_svn = None
267 self.svn_branch = None
268 self.tree_status_url = None
269 self.viewvc_url = None
270 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000271 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000272 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000273
274 def LazyUpdateIfNeeded(self):
275 """Updates the settings from a codereview.settings file, if available."""
276 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000277 # The only value that actually changes the behavior is
278 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000279 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000280 error_ok=True
281 ).strip().lower()
282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000283 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000284 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000286 # set updated to True to avoid infinite calling loop
287 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000288 self.updated = True
289 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 self.updated = True
291
292 def GetDefaultServerUrl(self, error_ok=False):
293 if not self.default_server:
294 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000295 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000296 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 if error_ok:
298 return self.default_server
299 if not self.default_server:
300 error_message = ('Could not find settings file. You must configure '
301 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000302 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000303 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000304 return self.default_server
305
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000306 def GetRelativeRoot(self):
307 if self.relative_root is None:
308 self.relative_root = RunGit(['rev-parse', '--show-cdup']).strip()
309 return self.relative_root
310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000311 def GetRoot(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000312 return os.path.abspath(self.GetRelativeRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000313
314 def GetIsGitSvn(self):
315 """Return true if this repo looks like it's using git-svn."""
316 if self.is_git_svn is None:
317 # If you have any "svn-remote.*" config keys, we think you're using svn.
318 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000319 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000320 return self.is_git_svn
321
322 def GetSVNBranch(self):
323 if self.svn_branch is None:
324 if not self.GetIsGitSvn():
325 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
326
327 # Try to figure out which remote branch we're based on.
328 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000329 # 1) iterate through our branch history and find the svn URL.
330 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000331
332 # regexp matching the git-svn line that contains the URL.
333 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
334
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000335 # We don't want to go through all of history, so read a line from the
336 # pipe at a time.
337 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000338 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000339 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
340 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000341 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000342 for line in proc.stdout:
343 match = git_svn_re.match(line)
344 if match:
345 url = match.group(1)
346 proc.stdout.close() # Cut pipe.
347 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000349 if url:
350 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
351 remotes = RunGit(['config', '--get-regexp',
352 r'^svn-remote\..*\.url']).splitlines()
353 for remote in remotes:
354 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000355 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000356 remote = match.group(1)
357 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000358 rewrite_root = RunGit(
359 ['config', 'svn-remote.%s.rewriteRoot' % remote],
360 error_ok=True).strip()
361 if rewrite_root:
362 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000363 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000364 ['config', 'svn-remote.%s.fetch' % remote],
365 error_ok=True).strip()
366 if fetch_spec:
367 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
368 if self.svn_branch:
369 break
370 branch_spec = RunGit(
371 ['config', 'svn-remote.%s.branches' % remote],
372 error_ok=True).strip()
373 if branch_spec:
374 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
375 if self.svn_branch:
376 break
377 tag_spec = RunGit(
378 ['config', 'svn-remote.%s.tags' % remote],
379 error_ok=True).strip()
380 if tag_spec:
381 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
382 if self.svn_branch:
383 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000384
385 if not self.svn_branch:
386 DieWithError('Can\'t guess svn branch -- try specifying it on the '
387 'command line')
388
389 return self.svn_branch
390
391 def GetTreeStatusUrl(self, error_ok=False):
392 if not self.tree_status_url:
393 error_message = ('You must configure your tree status URL by running '
394 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000395 self.tree_status_url = self._GetRietveldConfig(
396 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000397 return self.tree_status_url
398
399 def GetViewVCUrl(self):
400 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000401 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 return self.viewvc_url
403
rmistry@google.com90752582014-01-14 21:04:50 +0000404 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000405 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000406
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000407 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000408 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000409
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000410 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000411 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000412
ukai@chromium.orge8077812012-02-03 03:41:46 +0000413 def GetIsGerrit(self):
414 """Return true if this repo is assosiated with gerrit code review system."""
415 if self.is_gerrit is None:
416 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
417 return self.is_gerrit
418
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000419 def GetGitEditor(self):
420 """Return the editor specified in the git config, or None if none is."""
421 if self.git_editor is None:
422 self.git_editor = self._GetConfig('core.editor', error_ok=True)
423 return self.git_editor or None
424
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000425 def _GetRietveldConfig(self, param, **kwargs):
426 return self._GetConfig('rietveld.' + param, **kwargs)
427
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 def _GetConfig(self, param, **kwargs):
429 self.LazyUpdateIfNeeded()
430 return RunGit(['config', param], **kwargs).strip()
431
432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433def ShortBranchName(branch):
434 """Convert a name like 'refs/heads/foo' to just 'foo'."""
435 return branch.replace('refs/heads/', '')
436
437
438class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000439 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000441 global settings
442 if not settings:
443 # Happens when git_cl.py is used as a utility library.
444 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000445 settings.GetDefaultServerUrl()
446 self.branchref = branchref
447 if self.branchref:
448 self.branch = ShortBranchName(self.branchref)
449 else:
450 self.branch = None
451 self.rietveld_server = None
452 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000453 self.lookedup_issue = False
454 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000455 self.has_description = False
456 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000457 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000459 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000460 self.cc = None
461 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000462 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000463 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000464
465 def GetCCList(self):
466 """Return the users cc'd on this CL.
467
468 Return is a string suitable for passing to gcl with the --cc flag.
469 """
470 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000471 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000472 more_cc = ','.join(self.watchers)
473 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
474 return self.cc
475
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000476 def GetCCListWithoutDefault(self):
477 """Return the users cc'd on this CL excluding default ones."""
478 if self.cc is None:
479 self.cc = ','.join(self.watchers)
480 return self.cc
481
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000482 def SetWatchers(self, watchers):
483 """Set the list of email addresses that should be cc'd based on the changed
484 files in this CL.
485 """
486 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000487
488 def GetBranch(self):
489 """Returns the short branch name, e.g. 'master'."""
490 if not self.branch:
491 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
492 self.branch = ShortBranchName(self.branchref)
493 return self.branch
494
495 def GetBranchRef(self):
496 """Returns the full branch name, e.g. 'refs/heads/master'."""
497 self.GetBranch() # Poke the lazy loader.
498 return self.branchref
499
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000500 @staticmethod
501 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000502 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503 e.g. 'origin', 'refs/heads/master'
504 """
505 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000506 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
507 error_ok=True).strip()
508 if upstream_branch:
509 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
510 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000511 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
512 error_ok=True).strip()
513 if upstream_branch:
514 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000515 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000516 # Fall back on trying a git-svn upstream branch.
517 if settings.GetIsGitSvn():
518 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000519 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000520 # Else, try to guess the origin remote.
521 remote_branches = RunGit(['branch', '-r']).split()
522 if 'origin/master' in remote_branches:
523 # Fall back on origin/master if it exits.
524 remote = 'origin'
525 upstream_branch = 'refs/heads/master'
526 elif 'origin/trunk' in remote_branches:
527 # Fall back on origin/trunk if it exists. Generally a shared
528 # git-svn clone
529 remote = 'origin'
530 upstream_branch = 'refs/heads/trunk'
531 else:
532 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000533Either pass complete "git diff"-style arguments, like
534 git cl upload origin/master
535or verify this branch is set up to track another (via the --track argument to
536"git checkout -b ...").""")
537
538 return remote, upstream_branch
539
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000540 def GetCommonAncestorWithUpstream(self):
541 return RunGit(['merge-base', self.GetUpstreamBranch(), 'HEAD']).strip()
542
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000543 def GetUpstreamBranch(self):
544 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000545 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000546 if remote is not '.':
547 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
548 self.upstream_branch = upstream_branch
549 return self.upstream_branch
550
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000551 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000552 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000553 remote, branch = None, self.GetBranch()
554 seen_branches = set()
555 while branch not in seen_branches:
556 seen_branches.add(branch)
557 remote, branch = self.FetchUpstreamTuple(branch)
558 branch = ShortBranchName(branch)
559 if remote != '.' or branch.startswith('refs/remotes'):
560 break
561 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000562 remotes = RunGit(['remote'], error_ok=True).split()
563 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000564 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000565 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000566 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000567 logging.warning('Could not determine which remote this change is '
568 'associated with, so defaulting to "%s". This may '
569 'not be what you want. You may prevent this message '
570 'by running "git svn info" as documented here: %s',
571 self._remote,
572 GIT_INSTRUCTIONS_URL)
573 else:
574 logging.warn('Could not determine which remote this change is '
575 'associated with. You may prevent this message by '
576 'running "git svn info" as documented here: %s',
577 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000578 branch = 'HEAD'
579 if branch.startswith('refs/remotes'):
580 self._remote = (remote, branch)
581 else:
582 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000583 return self._remote
584
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000585 def GitSanityChecks(self, upstream_git_obj):
586 """Checks git repo status and ensures diff is from local commits."""
587
588 # Verify the commit we're diffing against is in our current branch.
589 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
590 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
591 if upstream_sha != common_ancestor:
592 print >> sys.stderr, (
593 'ERROR: %s is not in the current branch. You may need to rebase '
594 'your tracking branch' % upstream_sha)
595 return False
596
597 # List the commits inside the diff, and verify they are all local.
598 commits_in_diff = RunGit(
599 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
600 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
601 remote_branch = remote_branch.strip()
602 if code != 0:
603 _, remote_branch = self.GetRemoteBranch()
604
605 commits_in_remote = RunGit(
606 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
607
608 common_commits = set(commits_in_diff) & set(commits_in_remote)
609 if common_commits:
610 print >> sys.stderr, (
611 'ERROR: Your diff contains %d commits already in %s.\n'
612 'Run "git log --oneline %s..HEAD" to get a list of commits in '
613 'the diff. If you are using a custom git flow, you can override'
614 ' the reference used for this check with "git config '
615 'gitcl.remotebranch <git-ref>".' % (
616 len(common_commits), remote_branch, upstream_git_obj))
617 return False
618 return True
619
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000620 def GetGitBaseUrlFromConfig(self):
621 """Return the configured base URL from branch.<branchname>.baseurl.
622
623 Returns None if it is not set.
624 """
625 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
626 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 def GetRemoteUrl(self):
629 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
630
631 Returns None if there is no remote.
632 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000633 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
635
636 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000637 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000638 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000640 self.issue = int(issue) or None if issue else None
641 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000642 return self.issue
643
644 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000645 if not self.rietveld_server:
646 # If we're on a branch then get the server potentially associated
647 # with that branch.
648 if self.GetIssue():
649 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
650 ['config', self._RietveldServer()], error_ok=True).strip())
651 if not self.rietveld_server:
652 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653 return self.rietveld_server
654
655 def GetIssueURL(self):
656 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000657 if not self.GetIssue():
658 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
660
661 def GetDescription(self, pretty=False):
662 if not self.has_description:
663 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000664 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000665 try:
666 self.description = self.RpcServer().get_description(issue).strip()
667 except urllib2.HTTPError, e:
668 if e.code == 404:
669 DieWithError(
670 ('\nWhile fetching the description for issue %d, received a '
671 '404 (not found)\n'
672 'error. It is likely that you deleted this '
673 'issue on the server. If this is the\n'
674 'case, please run\n\n'
675 ' git cl issue 0\n\n'
676 'to clear the association with the deleted issue. Then run '
677 'this command again.') % issue)
678 else:
679 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000680 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000681 self.has_description = True
682 if pretty:
683 wrapper = textwrap.TextWrapper()
684 wrapper.initial_indent = wrapper.subsequent_indent = ' '
685 return wrapper.fill(self.description)
686 return self.description
687
688 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000689 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000690 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 patchset = RunGit(['config', self._PatchsetSetting()],
692 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000693 self.patchset = int(patchset) or None if patchset else None
694 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 return self.patchset
696
697 def SetPatchset(self, patchset):
698 """Set this branch's patchset. If patchset=0, clears the patchset."""
699 if patchset:
700 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000701 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702 else:
703 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000704 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000705 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000707 def GetMostRecentPatchset(self):
708 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000709
710 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000711 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000712 '/download/issue%s_%s.diff' % (issue, patchset))
713
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000714 def GetIssueProperties(self):
715 if self._props is None:
716 issue = self.GetIssue()
717 if not issue:
718 self._props = {}
719 else:
720 self._props = self.RpcServer().get_issue_properties(issue, True)
721 return self._props
722
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000723 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000724 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000725
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726 def SetIssue(self, issue):
727 """Set this branch's issue. If issue=0, clears the issue."""
728 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000729 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730 RunGit(['config', self._IssueSetting(), str(issue)])
731 if self.rietveld_server:
732 RunGit(['config', self._RietveldServer(), self.rietveld_server])
733 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000734 current_issue = self.GetIssue()
735 if current_issue:
736 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000737 self.issue = None
738 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000740 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000741 if not self.GitSanityChecks(upstream_branch):
742 DieWithError('\nGit sanity check failure')
743
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000744 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000745 if not root:
746 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000747 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000748
749 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000750 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000751 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000752 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000753 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000754 except subprocess2.CalledProcessError:
755 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000756 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000757 'This branch probably doesn\'t exist anymore. To reset the\n'
758 'tracking branch, please run\n'
759 ' git branch --set-upstream %s trunk\n'
760 'replacing trunk with origin/master or the relevant branch') %
761 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000762
maruel@chromium.org52424302012-08-29 15:14:30 +0000763 issue = self.GetIssue()
764 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000765 if issue:
766 description = self.GetDescription()
767 else:
768 # If the change was never uploaded, use the log messages of all commits
769 # up to the branch point, as git cl upload will prefill the description
770 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000771 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
772 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000773
774 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000775 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000776 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000777 name,
778 description,
779 absroot,
780 files,
781 issue,
782 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000783 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000784
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000785 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000786 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000787
788 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000789 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000790 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000791 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000792 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000793 except presubmit_support.PresubmitFailure, e:
794 DieWithError(
795 ('%s\nMaybe your depot_tools is out of date?\n'
796 'If all fails, contact maruel@') % e)
797
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000798 def UpdateDescription(self, description):
799 self.description = description
800 return self.RpcServer().update_description(
801 self.GetIssue(), self.description)
802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000804 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000805 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000807 def SetFlag(self, flag, value):
808 """Patchset must match."""
809 if not self.GetPatchset():
810 DieWithError('The patchset needs to match. Send another patchset.')
811 try:
812 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000813 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000814 except urllib2.HTTPError, e:
815 if e.code == 404:
816 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
817 if e.code == 403:
818 DieWithError(
819 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
820 'match?') % (self.GetIssue(), self.GetPatchset()))
821 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000823 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 """Returns an upload.RpcServer() to access this review's rietveld instance.
825 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000826 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000827 self._rpc_server = rietveld.CachingRietveld(
828 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000829 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830
831 def _IssueSetting(self):
832 """Return the git setting that stores this change's issue."""
833 return 'branch.%s.rietveldissue' % self.GetBranch()
834
835 def _PatchsetSetting(self):
836 """Return the git setting that stores this change's most recent patchset."""
837 return 'branch.%s.rietveldpatchset' % self.GetBranch()
838
839 def _RietveldServer(self):
840 """Returns the git setting that stores this change's rietveld server."""
841 return 'branch.%s.rietveldserver' % self.GetBranch()
842
843
844def GetCodereviewSettingsInteractively():
845 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000846 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847 server = settings.GetDefaultServerUrl(error_ok=True)
848 prompt = 'Rietveld server (host[:port])'
849 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000850 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851 if not server and not newserver:
852 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000853 if newserver:
854 newserver = gclient_utils.UpgradeToHttps(newserver)
855 if newserver != server:
856 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000858 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 prompt = caption
860 if initial:
861 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000862 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 if new_val == 'x':
864 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000865 elif new_val:
866 if is_url:
867 new_val = gclient_utils.UpgradeToHttps(new_val)
868 if new_val != initial:
869 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000871 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000872 SetProperty(settings.GetDefaultPrivateFlag(),
873 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000875 'tree-status-url', False)
876 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000877 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878
879 # TODO: configure a default branch to diff against, rather than this
880 # svn-based hackery.
881
882
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000883class ChangeDescription(object):
884 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000885 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000886 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000887
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000888 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000889 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000890
agable@chromium.org42c20792013-09-12 17:34:49 +0000891 @property # www.logilab.org/ticket/89786
892 def description(self): # pylint: disable=E0202
893 return '\n'.join(self._description_lines)
894
895 def set_description(self, desc):
896 if isinstance(desc, basestring):
897 lines = desc.splitlines()
898 else:
899 lines = [line.rstrip() for line in desc]
900 while lines and not lines[0]:
901 lines.pop(0)
902 while lines and not lines[-1]:
903 lines.pop(-1)
904 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000905
906 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000907 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000908 assert isinstance(reviewers, list), reviewers
909 if not reviewers:
910 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000911 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000912
agable@chromium.org42c20792013-09-12 17:34:49 +0000913 # Get the set of R= and TBR= lines and remove them from the desciption.
914 regexp = re.compile(self.R_LINE)
915 matches = [regexp.match(line) for line in self._description_lines]
916 new_desc = [l for i, l in enumerate(self._description_lines)
917 if not matches[i]]
918 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000919
agable@chromium.org42c20792013-09-12 17:34:49 +0000920 # Construct new unified R= and TBR= lines.
921 r_names = []
922 tbr_names = []
923 for match in matches:
924 if not match:
925 continue
926 people = cleanup_list([match.group(2).strip()])
927 if match.group(1) == 'TBR':
928 tbr_names.extend(people)
929 else:
930 r_names.extend(people)
931 for name in r_names:
932 if name not in reviewers:
933 reviewers.append(name)
934 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
935 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
936
937 # Put the new lines in the description where the old first R= line was.
938 line_loc = next((i for i, match in enumerate(matches) if match), -1)
939 if 0 <= line_loc < len(self._description_lines):
940 if new_tbr_line:
941 self._description_lines.insert(line_loc, new_tbr_line)
942 if new_r_line:
943 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000944 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000945 if new_r_line:
946 self.append_footer(new_r_line)
947 if new_tbr_line:
948 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000949
950 def prompt(self):
951 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000952 self.set_description([
953 '# Enter a description of the change.',
954 '# This will be displayed on the codereview site.',
955 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000956 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000957 '--------------------',
958 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000959
agable@chromium.org42c20792013-09-12 17:34:49 +0000960 regexp = re.compile(self.BUG_LINE)
961 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000962 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000964 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000965 if not content:
966 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000967 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000968
969 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000970 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
971 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000972 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000973 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000974
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000975 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000976 if self._description_lines:
977 # Add an empty line if either the last line or the new line isn't a tag.
978 last_line = self._description_lines[-1]
979 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
980 not presubmit_support.Change.TAG_LINE_RE.match(line)):
981 self._description_lines.append('')
982 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000983
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000984 def get_reviewers(self):
985 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000986 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
987 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000988 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000989
990
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000991def get_approving_reviewers(props):
992 """Retrieves the reviewers that approved a CL from the issue properties with
993 messages.
994
995 Note that the list may contain reviewers that are not committer, thus are not
996 considered by the CQ.
997 """
998 return sorted(
999 set(
1000 message['sender']
1001 for message in props['messages']
1002 if message['approval'] and message['sender'] in props['reviewers']
1003 )
1004 )
1005
1006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007def FindCodereviewSettingsFile(filename='codereview.settings'):
1008 """Finds the given file starting in the cwd and going up.
1009
1010 Only looks up to the top of the repository unless an
1011 'inherit-review-settings-ok' file exists in the root of the repository.
1012 """
1013 inherit_ok_file = 'inherit-review-settings-ok'
1014 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001015 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1017 root = '/'
1018 while True:
1019 if filename in os.listdir(cwd):
1020 if os.path.isfile(os.path.join(cwd, filename)):
1021 return open(os.path.join(cwd, filename))
1022 if cwd == root:
1023 break
1024 cwd = os.path.dirname(cwd)
1025
1026
1027def LoadCodereviewSettingsFromFile(fileobj):
1028 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001029 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 def SetProperty(name, setting, unset_error_ok=False):
1032 fullname = 'rietveld.' + name
1033 if setting in keyvals:
1034 RunGit(['config', fullname, keyvals[setting]])
1035 else:
1036 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1037
1038 SetProperty('server', 'CODE_REVIEW_SERVER')
1039 # Only server setting is required. Other settings can be absent.
1040 # In that case, we ignore errors raised during option deletion attempt.
1041 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001042 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1044 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001045 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001047 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001048 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1051 #should be of the form
1052 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1053 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1054 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1055 keyvals['ORIGIN_URL_CONFIG']])
1056
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001058def urlretrieve(source, destination):
1059 """urllib is broken for SSL connections via a proxy therefore we
1060 can't use urllib.urlretrieve()."""
1061 with open(destination, 'w') as f:
1062 f.write(urllib2.urlopen(source).read())
1063
1064
ukai@chromium.org712d6102013-11-27 00:52:58 +00001065def hasSheBang(fname):
1066 """Checks fname is a #! script."""
1067 with open(fname) as f:
1068 return f.read(2).startswith('#!')
1069
1070
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001071def DownloadHooks(force):
1072 """downloads hooks
1073
1074 Args:
1075 force: True to update hooks. False to install hooks if not present.
1076 """
1077 if not settings.GetIsGerrit():
1078 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001079 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001080 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1081 if not os.access(dst, os.X_OK):
1082 if os.path.exists(dst):
1083 if not force:
1084 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001085 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001086 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001087 if not hasSheBang(dst):
1088 DieWithError('Not a script: %s\n'
1089 'You need to download from\n%s\n'
1090 'into .git/hooks/commit-msg and '
1091 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001092 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1093 except Exception:
1094 if os.path.exists(dst):
1095 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001096 DieWithError('\nFailed to download hooks.\n'
1097 'You need to download from\n%s\n'
1098 'into .git/hooks/commit-msg and '
1099 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001100
1101
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001102@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001104 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001106 parser.add_option('--activate-update', action='store_true',
1107 help='activate auto-updating [rietveld] section in '
1108 '.git/config')
1109 parser.add_option('--deactivate-update', action='store_true',
1110 help='deactivate auto-updating [rietveld] section in '
1111 '.git/config')
1112 options, args = parser.parse_args(args)
1113
1114 if options.deactivate_update:
1115 RunGit(['config', 'rietveld.autoupdate', 'false'])
1116 return
1117
1118 if options.activate_update:
1119 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1120 return
1121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 if len(args) == 0:
1123 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001124 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 return 0
1126
1127 url = args[0]
1128 if not url.endswith('codereview.settings'):
1129 url = os.path.join(url, 'codereview.settings')
1130
1131 # Load code review settings and download hooks (if available).
1132 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001133 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 return 0
1135
1136
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001137def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001138 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001139 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1140 branch = ShortBranchName(branchref)
1141 _, args = parser.parse_args(args)
1142 if not args:
1143 print("Current base-url:")
1144 return RunGit(['config', 'branch.%s.base-url' % branch],
1145 error_ok=False).strip()
1146 else:
1147 print("Setting base-url to %s" % args[0])
1148 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1149 error_ok=False).strip()
1150
1151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001153 """Show status of changelists.
1154
1155 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001156 - Red not sent for review or broken
1157 - Blue waiting for review
1158 - Yellow waiting for you to reply to review
1159 - Green LGTM'ed
1160 - Magenta in the commit queue
1161 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001162
1163 Also see 'git cl comments'.
1164 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 parser.add_option('--field',
1166 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001167 parser.add_option('-f', '--fast', action='store_true',
1168 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001170 if args:
1171 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001174 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175 if options.field.startswith('desc'):
1176 print cl.GetDescription()
1177 elif options.field == 'id':
1178 issueid = cl.GetIssue()
1179 if issueid:
1180 print issueid
1181 elif options.field == 'patch':
1182 patchset = cl.GetPatchset()
1183 if patchset:
1184 print patchset
1185 elif options.field == 'url':
1186 url = cl.GetIssueURL()
1187 if url:
1188 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001189 return 0
1190
1191 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1192 if not branches:
1193 print('No local branch found.')
1194 return 0
1195
1196 changes = (Changelist(branchref=b) for b in branches.splitlines())
1197 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1198 alignment = max(5, max(len(b) for b in branches))
1199 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001200 # Adhoc thread pool to request data concurrently.
1201 output = Queue.Queue()
1202
1203 # Silence upload.py otherwise it becomes unweldly.
1204 upload.verbosity = 0
1205
1206 if not options.fast:
1207 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001208 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001209 c = Changelist(branchref=b)
1210 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001211 props = {}
1212 r = None
1213 if i:
1214 try:
1215 props = c.GetIssueProperties()
1216 r = c.GetApprovingReviewers() if i else None
1217 except urllib2.HTTPError:
1218 # The issue probably doesn't exist anymore.
1219 i += ' (broken)'
1220
1221 msgs = props.get('messages') or []
1222
1223 if not i:
1224 color = Fore.WHITE
1225 elif props.get('closed'):
1226 # Issue is closed.
1227 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001228 elif props.get('commit'):
1229 # Issue is in the commit queue.
1230 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001231 elif r:
1232 # Was LGTM'ed.
1233 color = Fore.GREEN
1234 elif not msgs:
1235 # No message was sent.
1236 color = Fore.RED
1237 elif msgs[-1]['sender'] != props.get('owner_email'):
1238 color = Fore.YELLOW
1239 else:
1240 color = Fore.BLUE
1241 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001242
1243 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1244 for t in threads:
1245 t.daemon = True
1246 t.start()
1247 else:
1248 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1249 for b in branches:
1250 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001251 url = c.GetIssueURL()
1252 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001253
1254 tmp = {}
1255 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001256 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001257 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001258 b, i, color = output.get()
1259 tmp[b] = (i, color)
1260 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001261 reset = Fore.RESET
1262 if not sys.stdout.isatty():
1263 color = ''
1264 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001265 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001266 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001267
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001268 cl = Changelist()
1269 print
1270 print 'Current branch:',
1271 if not cl.GetIssue():
1272 print 'no issue assigned.'
1273 return 0
1274 print cl.GetBranch()
1275 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1276 print 'Issue description:'
1277 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 return 0
1279
1280
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001281def colorize_CMDstatus_doc():
1282 """To be called once in main() to add colors to git cl status help."""
1283 colors = [i for i in dir(Fore) if i[0].isupper()]
1284
1285 def colorize_line(line):
1286 for color in colors:
1287 if color in line.upper():
1288 # Extract whitespaces first and the leading '-'.
1289 indent = len(line) - len(line.lstrip(' ')) + 1
1290 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1291 return line
1292
1293 lines = CMDstatus.__doc__.splitlines()
1294 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1295
1296
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001297@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001299 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300
1301 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001302 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001303 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304
1305 cl = Changelist()
1306 if len(args) > 0:
1307 try:
1308 issue = int(args[0])
1309 except ValueError:
1310 DieWithError('Pass a number to set the issue or none to list it.\n'
1311 'Maybe you want to run git cl status?')
1312 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001313 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 return 0
1315
1316
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001317def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001318 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001319 (_, args) = parser.parse_args(args)
1320 if args:
1321 parser.error('Unsupported argument: %s' % args)
1322
1323 cl = Changelist()
1324 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001325 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001326 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001327 if message['disapproval']:
1328 color = Fore.RED
1329 elif message['approval']:
1330 color = Fore.GREEN
1331 elif message['sender'] == data['owner_email']:
1332 color = Fore.MAGENTA
1333 else:
1334 color = Fore.BLUE
1335 print '\n%s%s %s%s' % (
1336 color, message['date'].split('.', 1)[0], message['sender'],
1337 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001338 if message['text'].strip():
1339 print '\n'.join(' ' + l for l in message['text'].splitlines())
1340 return 0
1341
1342
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001343def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001344 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001345 cl = Changelist()
1346 if not cl.GetIssue():
1347 DieWithError('This branch has no associated changelist.')
1348 description = ChangeDescription(cl.GetDescription())
1349 description.prompt()
1350 cl.UpdateDescription(description.description)
1351 return 0
1352
1353
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354def CreateDescriptionFromLog(args):
1355 """Pulls out the commit log to use as a base for the CL description."""
1356 log_args = []
1357 if len(args) == 1 and not args[0].endswith('.'):
1358 log_args = [args[0] + '..']
1359 elif len(args) == 1 and args[0].endswith('...'):
1360 log_args = [args[0][:-1]]
1361 elif len(args) == 2:
1362 log_args = [args[0] + '..' + args[1]]
1363 else:
1364 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001365 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
1367
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001369 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001370 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001372 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001373 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 (options, args) = parser.parse_args(args)
1375
ukai@chromium.org259e4682012-10-25 07:36:33 +00001376 if not options.force and is_dirty_git_tree('presubmit'):
1377 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 return 1
1379
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001380 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 if args:
1382 base_branch = args[0]
1383 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001385 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001387 cl.RunHook(
1388 committing=not options.upload,
1389 may_prompt=False,
1390 verbose=options.verbose,
1391 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001392 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
1394
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001395def AddChangeIdToCommitMessage(options, args):
1396 """Re-commits using the current message, assumes the commit hook is in
1397 place.
1398 """
1399 log_desc = options.message or CreateDescriptionFromLog(args)
1400 git_command = ['commit', '--amend', '-m', log_desc]
1401 RunGit(git_command)
1402 new_log_desc = CreateDescriptionFromLog(args)
1403 if CHANGE_ID in new_log_desc:
1404 print 'git-cl: Added Change-Id to commit message.'
1405 else:
1406 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1407
1408
ukai@chromium.orge8077812012-02-03 03:41:46 +00001409def GerritUpload(options, args, cl):
1410 """upload the current branch to gerrit."""
1411 # We assume the remote called "origin" is the one we want.
1412 # It is probably not worthwhile to support different workflows.
1413 remote = 'origin'
1414 branch = 'master'
1415 if options.target_branch:
1416 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001418 change_desc = ChangeDescription(
1419 options.message or CreateDescriptionFromLog(args))
1420 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001421 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001423 if CHANGE_ID not in change_desc.description:
1424 AddChangeIdToCommitMessage(options, args)
1425 if options.reviewers:
1426 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
ukai@chromium.orge8077812012-02-03 03:41:46 +00001428 receive_options = []
1429 cc = cl.GetCCList().split(',')
1430 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001431 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001432 cc = filter(None, cc)
1433 if cc:
1434 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001435 if change_desc.get_reviewers():
1436 receive_options.extend(
1437 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438
ukai@chromium.orge8077812012-02-03 03:41:46 +00001439 git_command = ['push']
1440 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001441 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001442 ' '.join(receive_options))
1443 git_command += [remote, 'HEAD:refs/for/' + branch]
1444 RunGit(git_command)
1445 # TODO(ukai): parse Change-Id: and set issue number?
1446 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001447
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448
ukai@chromium.orge8077812012-02-03 03:41:46 +00001449def RietveldUpload(options, args, cl):
1450 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1452 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 if options.emulate_svn_auto_props:
1454 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455
1456 change_desc = None
1457
pgervais@chromium.org91141372014-01-09 23:27:20 +00001458 if options.email is not None:
1459 upload_args.extend(['--email', options.email])
1460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001462 if options.title:
1463 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001464 if options.message:
1465 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001466 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 print ("This branch is associated with issue %s. "
1468 "Adding patch to that issue." % cl.GetIssue())
1469 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001470 if options.title:
1471 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001472 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 change_desc = ChangeDescription(message)
1474 if options.reviewers:
1475 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001476 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001477 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001478
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001479 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480 print "Description is empty; aborting."
1481 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001482
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001483 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001484 if change_desc.get_reviewers():
1485 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001486 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001487 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001488 DieWithError("Must specify reviewers to send email.")
1489 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001490
1491 # We check this before applying rietveld.private assuming that in
1492 # rietveld.cc only addresses which we can send private CLs to are listed
1493 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1494 # --private is specified explicitly on the command line.
1495 if options.private:
1496 logging.warn('rietveld.cc is ignored since private flag is specified. '
1497 'You need to review and add them manually if necessary.')
1498 cc = cl.GetCCListWithoutDefault()
1499 else:
1500 cc = cl.GetCCList()
1501 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001502 if cc:
1503 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001505 if options.private or settings.GetDefaultPrivateFlag() == "True":
1506 upload_args.append('--private')
1507
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001508 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001509 if not options.find_copies:
1510 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001511
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001512 # Include the upstream repo's URL in the change -- this is useful for
1513 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001514 remote_url = cl.GetGitBaseUrlFromConfig()
1515 if not remote_url:
1516 if settings.GetIsGitSvn():
1517 # URL is dependent on the current directory.
1518 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1519 if data:
1520 keys = dict(line.split(': ', 1) for line in data.splitlines()
1521 if ': ' in line)
1522 remote_url = keys.get('URL', None)
1523 else:
1524 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1525 remote_url = (cl.GetRemoteUrl() + '@'
1526 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 if remote_url:
1528 upload_args.extend(['--base_url', remote_url])
1529
1530 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001531 upload_args = ['upload'] + upload_args + args
1532 logging.info('upload.RealMain(%s)', upload_args)
1533 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001534 issue = int(issue)
1535 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001536 except KeyboardInterrupt:
1537 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001538 except:
1539 # If we got an exception after the user typed a description for their
1540 # change, back up the description before re-raising.
1541 if change_desc:
1542 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1543 print '\nGot exception while uploading -- saving description to %s\n' \
1544 % backup_path
1545 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001546 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 backup_file.close()
1548 raise
1549
1550 if not cl.GetIssue():
1551 cl.SetIssue(issue)
1552 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001553
1554 if options.use_commit_queue:
1555 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556 return 0
1557
1558
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001559def cleanup_list(l):
1560 """Fixes a list so that comma separated items are put as individual items.
1561
1562 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1563 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1564 """
1565 items = sum((i.split(',') for i in l), [])
1566 stripped_items = (i.strip() for i in items)
1567 return sorted(filter(None, stripped_items))
1568
1569
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001570@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001571def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001572 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1574 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001575 parser.add_option('--bypass-watchlists', action='store_true',
1576 dest='bypass_watchlists',
1577 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001578 parser.add_option('-f', action='store_true', dest='force',
1579 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001580 parser.add_option('-m', dest='message', help='message for patchset')
1581 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001582 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001583 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001584 help='reviewer email addresses')
1585 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001586 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001587 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001588 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001589 help='send email to reviewer immediately')
1590 parser.add_option("--emulate_svn_auto_props", action="store_true",
1591 dest="emulate_svn_auto_props",
1592 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001593 parser.add_option('-c', '--use-commit-queue', action='store_true',
1594 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001595 parser.add_option('--private', action='store_true',
1596 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001597 parser.add_option('--target_branch',
1598 help='When uploading to gerrit, remote branch to '
1599 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001600 parser.add_option('--email', default=None,
1601 help='email address to use to connect to Rietveld')
1602
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001603 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001604 (options, args) = parser.parse_args(args)
1605
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001606 if options.target_branch and not settings.GetIsGerrit():
1607 parser.error('Use --target_branch for non gerrit repository.')
1608
ukai@chromium.org259e4682012-10-25 07:36:33 +00001609 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001610 return 1
1611
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001612 options.reviewers = cleanup_list(options.reviewers)
1613 options.cc = cleanup_list(options.cc)
1614
ukai@chromium.orge8077812012-02-03 03:41:46 +00001615 cl = Changelist()
1616 if args:
1617 # TODO(ukai): is it ok for gerrit case?
1618 base_branch = args[0]
1619 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001620 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001621 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001622 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001623
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001624 # Apply watchlists on upload.
1625 change = cl.GetChange(base_branch, None)
1626 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1627 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001628 if not options.bypass_watchlists:
1629 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001630
ukai@chromium.orge8077812012-02-03 03:41:46 +00001631 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001632 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001633 may_prompt=not options.force,
1634 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001635 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001636 if not hook_results.should_continue():
1637 return 1
1638 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001639 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001640
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001641 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001642 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001643 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001644 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001645 print ('The last upload made from this repository was patchset #%d but '
1646 'the most recent patchset on the server is #%d.'
1647 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001648 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1649 'from another machine or branch the patch you\'re uploading now '
1650 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001651 ask_for_data('About to upload; enter to confirm.')
1652
iannucci@chromium.org79540052012-10-19 23:15:26 +00001653 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001654 if settings.GetIsGerrit():
1655 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001656 ret = RietveldUpload(options, args, cl)
1657 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001658 git_set_branch_value('last-upload-hash',
1659 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001660
1661 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001662
1663
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001664def IsSubmoduleMergeCommit(ref):
1665 # When submodules are added to the repo, we expect there to be a single
1666 # non-git-svn merge commit at remote HEAD with a signature comment.
1667 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001668 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001669 return RunGit(cmd) != ''
1670
1671
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672def SendUpstream(parser, args, cmd):
1673 """Common code for CmdPush and CmdDCommit
1674
1675 Squashed commit into a single.
1676 Updates changelog with metadata (e.g. pointer to review).
1677 Pushes/dcommits the code upstream.
1678 Updates review and closes.
1679 """
1680 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1681 help='bypass upload presubmit hook')
1682 parser.add_option('-m', dest='message',
1683 help="override review description")
1684 parser.add_option('-f', action='store_true', dest='force',
1685 help="force yes to questions (don't prompt)")
1686 parser.add_option('-c', dest='contributor',
1687 help="external contributor for patch (appended to " +
1688 "description and used as author for git). Should be " +
1689 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001690 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691 (options, args) = parser.parse_args(args)
1692 cl = Changelist()
1693
1694 if not args or cmd == 'push':
1695 # Default to merging against our best guess of the upstream branch.
1696 args = [cl.GetUpstreamBranch()]
1697
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001698 if options.contributor:
1699 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1700 print "Please provide contibutor as 'First Last <email@example.com>'"
1701 return 1
1702
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001703 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001704 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001705
ukai@chromium.org259e4682012-10-25 07:36:33 +00001706 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707 return 1
1708
1709 # This rev-list syntax means "show all commits not in my branch that
1710 # are in base_branch".
1711 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1712 base_branch]).splitlines()
1713 if upstream_commits:
1714 print ('Base branch "%s" has %d commits '
1715 'not in this branch.' % (base_branch, len(upstream_commits)))
1716 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1717 return 1
1718
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001719 # This is the revision `svn dcommit` will commit on top of.
1720 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1721 '--pretty=format:%H'])
1722
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001724 # If the base_head is a submodule merge commit, the first parent of the
1725 # base_head should be a git-svn commit, which is what we're interested in.
1726 base_svn_head = base_branch
1727 if base_has_submodules:
1728 base_svn_head += '^1'
1729
1730 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731 if extra_commits:
1732 print ('This branch has %d additional commits not upstreamed yet.'
1733 % len(extra_commits.splitlines()))
1734 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1735 'before attempting to %s.' % (base_branch, cmd))
1736 return 1
1737
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001738 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001739 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001740 author = None
1741 if options.contributor:
1742 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001743 hook_results = cl.RunHook(
1744 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001745 may_prompt=not options.force,
1746 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001747 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001748 if not hook_results.should_continue():
1749 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750
1751 if cmd == 'dcommit':
1752 # Check the tree status if the tree status URL is set.
1753 status = GetTreeStatus()
1754 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001755 print('The tree is closed. Please wait for it to reopen. Use '
1756 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001757 return 1
1758 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001759 print('Unable to determine tree status. Please verify manually and '
1760 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001761 else:
1762 breakpad.SendStack(
1763 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001764 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1765 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001766 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001768 change_desc = ChangeDescription(options.message)
1769 if not change_desc.description and cl.GetIssue():
1770 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001772 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001773 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001774 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001775 else:
1776 print 'No description set.'
1777 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1778 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001780 # Keep a separate copy for the commit message, because the commit message
1781 # contains the link to the Rietveld issue, while the Rietveld message contains
1782 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001783 # Keep a separate copy for the commit message.
1784 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001785 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001786
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001787 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001788 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001789 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001791 commit_desc.append_footer('Patch from %s.' % options.contributor)
1792
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001793 print('Description:')
1794 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795
1796 branches = [base_branch, cl.GetBranchRef()]
1797 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001798 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001799 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001801 # We want to squash all this branch's commits into one commit with the proper
1802 # description. We do this by doing a "reset --soft" to the base branch (which
1803 # keeps the working copy the same), then dcommitting that. If origin/master
1804 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1805 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001807 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1808 # Delete the branches if they exist.
1809 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1810 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1811 result = RunGitWithCode(showref_cmd)
1812 if result[0] == 0:
1813 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814
1815 # We might be in a directory that's present in this branch but not in the
1816 # trunk. Move up to the top of the tree so that git commands that expect a
1817 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001818 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819 if rel_base_path:
1820 os.chdir(rel_base_path)
1821
1822 # Stuff our change into the merge branch.
1823 # We wrap in a try...finally block so if anything goes wrong,
1824 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001825 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001826 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001827 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1828 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001830 RunGit(
1831 [
1832 'commit', '--author', options.contributor,
1833 '-m', commit_desc.description,
1834 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001836 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001837 if base_has_submodules:
1838 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1839 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1840 RunGit(['checkout', CHERRY_PICK_BRANCH])
1841 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842 if cmd == 'push':
1843 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001844 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 retcode, output = RunGitWithCode(
1846 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1847 logging.debug(output)
1848 else:
1849 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001850 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001851 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001852 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853 finally:
1854 # And then swap back to the original branch and clean up.
1855 RunGit(['checkout', '-q', cl.GetBranch()])
1856 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001857 if base_has_submodules:
1858 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859
1860 if cl.GetIssue():
1861 if cmd == 'dcommit' and 'Committed r' in output:
1862 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1863 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001864 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1865 for l in output.splitlines(False))
1866 match = filter(None, match)
1867 if len(match) != 1:
1868 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1869 output)
1870 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 else:
1872 return 1
1873 viewvc_url = settings.GetViewVCUrl()
1874 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001875 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001876 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001877 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001878 print ('Closing issue '
1879 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001880 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001882 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001883 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001884 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001885 if options.bypass_hooks:
1886 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1887 else:
1888 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001889 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001890 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001891
1892 if retcode == 0:
1893 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1894 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001895 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001896
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897 return 0
1898
1899
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001900@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001902 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001904 message = """This doesn't appear to be an SVN repository.
1905If your project has a git mirror with an upstream SVN master, you probably need
1906to run 'git svn init', see your project's git mirror documentation.
1907If your project has a true writeable upstream repository, you probably want
1908to run 'git cl push' instead.
1909Choose wisely, if you get this wrong, your commit might appear to succeed but
1910will instead be silently ignored."""
1911 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001912 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 return SendUpstream(parser, args, 'dcommit')
1914
1915
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001916@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001917def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001918 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919 if settings.GetIsGitSvn():
1920 print('This appears to be an SVN repository.')
1921 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001922 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923 return SendUpstream(parser, args, 'push')
1924
1925
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001926@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001928 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929 parser.add_option('-b', dest='newbranch',
1930 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001931 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001933 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1934 help='Change to the directory DIR immediately, '
1935 'before doing anything else.')
1936 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001937 help='failed patches spew .rej files rather than '
1938 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001939 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1940 help="don't commit after patch applies")
1941 (options, args) = parser.parse_args(args)
1942 if len(args) != 1:
1943 parser.print_help()
1944 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001945 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001947 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001948 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001949
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001950 if options.newbranch:
1951 if options.force:
1952 RunGit(['branch', '-D', options.newbranch],
1953 stderr=subprocess2.PIPE, error_ok=True)
1954 RunGit(['checkout', '-b', options.newbranch,
1955 Changelist().GetUpstreamBranch()])
1956
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001957 return PatchIssue(issue_arg, options.reject, options.nocommit,
1958 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001959
1960
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001961def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001962 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001964 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001965 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001966 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001967 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001969 # Assume it's a URL to the patch. Default to https.
1970 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001971 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001972 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 DieWithError('Must pass an issue ID or full URL for '
1974 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001975 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001976 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001977 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 # Switch up to the top-level directory, if necessary, in preparation for
1980 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001981 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001982 if top:
1983 os.chdir(top)
1984
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985 # Git patches have a/ at the beginning of source paths. We strip that out
1986 # with a sed script rather than the -p flag to patch so we can feed either
1987 # Git or svn-style patches into the same apply command.
1988 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001989 try:
1990 patch_data = subprocess2.check_output(
1991 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1992 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993 DieWithError('Git patch mungling failed.')
1994 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001996 # We use "git apply" to apply the patch instead of "patch" so that we can
1997 # pick up file adds.
1998 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001999 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002000 if directory:
2001 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002002 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002003 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002004 elif IsGitVersionAtLeast('1.7.12'):
2005 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002006 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002007 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002008 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002009 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002010 DieWithError('Failed to apply the patch')
2011
2012 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002013 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002014 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2015 cl = Changelist()
2016 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002017 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002018 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019 else:
2020 print "Patch applied to index."
2021 return 0
2022
2023
2024def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002025 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 # Provide a wrapper for git svn rebase to help avoid accidental
2027 # git svn dcommit.
2028 # It's the only command that doesn't use parser at all since we just defer
2029 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002030
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002031 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
2033
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002034def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002035 """Fetches the tree status and returns either 'open', 'closed',
2036 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002037 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038 if url:
2039 status = urllib2.urlopen(url).read().lower()
2040 if status.find('closed') != -1 or status == '0':
2041 return 'closed'
2042 elif status.find('open') != -1 or status == '1':
2043 return 'open'
2044 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045 return 'unset'
2046
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048def GetTreeStatusReason():
2049 """Fetches the tree status from a json url and returns the message
2050 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002051 url = settings.GetTreeStatusUrl()
2052 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053 connection = urllib2.urlopen(json_url)
2054 status = json.loads(connection.read())
2055 connection.close()
2056 return status['message']
2057
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002059def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002060 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002061 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062 status = GetTreeStatus()
2063 if 'unset' == status:
2064 print 'You must configure your tree status URL by running "git cl config".'
2065 return 2
2066
2067 print "The tree is %s" % status
2068 print
2069 print GetTreeStatusReason()
2070 if status != 'open':
2071 return 1
2072 return 0
2073
2074
maruel@chromium.org15192402012-09-06 12:38:29 +00002075def CMDtry(parser, args):
2076 """Triggers a try job through Rietveld."""
2077 group = optparse.OptionGroup(parser, "Try job options")
2078 group.add_option(
2079 "-b", "--bot", action="append",
2080 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2081 "times to specify multiple builders. ex: "
2082 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2083 "the try server waterfall for the builders name and the tests "
2084 "available. Can also be used to specify gtest_filter, e.g. "
2085 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2086 group.add_option(
2087 "-r", "--revision",
2088 help="Revision to use for the try job; default: the "
2089 "revision will be determined by the try server; see "
2090 "its waterfall for more info")
2091 group.add_option(
2092 "-c", "--clobber", action="store_true", default=False,
2093 help="Force a clobber before building; e.g. don't do an "
2094 "incremental build")
2095 group.add_option(
2096 "--project",
2097 help="Override which project to use. Projects are defined "
2098 "server-side to define what default bot set to use")
2099 group.add_option(
2100 "-t", "--testfilter", action="append", default=[],
2101 help=("Apply a testfilter to all the selected builders. Unless the "
2102 "builders configurations are similar, use multiple "
2103 "--bot <builder>:<test> arguments."))
2104 group.add_option(
2105 "-n", "--name", help="Try job name; default to current branch name")
2106 parser.add_option_group(group)
2107 options, args = parser.parse_args(args)
2108
2109 if args:
2110 parser.error('Unknown arguments: %s' % args)
2111
2112 cl = Changelist()
2113 if not cl.GetIssue():
2114 parser.error('Need to upload first')
2115
2116 if not options.name:
2117 options.name = cl.GetBranch()
2118
2119 # Process --bot and --testfilter.
2120 if not options.bot:
2121 # Get try slaves from PRESUBMIT.py files if not specified.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002122 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002123 options.bot = presubmit_support.DoGetTrySlaves(
2124 change,
2125 change.LocalPaths(),
2126 settings.GetRoot(),
2127 None,
2128 None,
2129 options.verbose,
2130 sys.stdout)
2131 if not options.bot:
2132 parser.error('No default try builder to try, use --bot')
2133
2134 builders_and_tests = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002135 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2136 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2137
2138 for bot in old_style:
maruel@chromium.org15192402012-09-06 12:38:29 +00002139 if ':' in bot:
2140 builder, tests = bot.split(':', 1)
2141 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2142 elif ',' in bot:
2143 parser.error('Specify one bot per --bot flag')
2144 else:
2145 builders_and_tests.setdefault(bot, []).append('defaulttests')
2146
stip@chromium.org43064fd2013-12-18 20:07:44 +00002147 for bot, tests in new_style:
2148 builders_and_tests.setdefault(bot, []).extend(tests)
2149
maruel@chromium.org15192402012-09-06 12:38:29 +00002150 if options.testfilter:
2151 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2152 builders_and_tests = dict(
2153 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2154 if t != ['compile'])
2155
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002156 if any('triggered' in b for b in builders_and_tests):
2157 print >> sys.stderr, (
2158 'ERROR You are trying to send a job to a triggered bot. This type of'
2159 ' bot requires an\ninitial job from a parent (usually a builder). '
2160 'Instead send your job to the parent.\n'
2161 'Bot list: %s' % builders_and_tests)
2162 return 1
2163
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002164 patchset = cl.GetMostRecentPatchset()
2165 if patchset and patchset != cl.GetPatchset():
2166 print(
2167 '\nWARNING Mismatch between local config and server. Did a previous '
2168 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2169 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002170 try:
2171 cl.RpcServer().trigger_try_jobs(
2172 cl.GetIssue(), patchset, options.name, options.clobber,
2173 options.revision, builders_and_tests)
2174 except urllib2.HTTPError, e:
2175 if e.code == 404:
2176 print('404 from rietveld; '
2177 'did you mean to use "git try" instead of "git cl try"?')
2178 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002179 print('Tried jobs on:')
2180 length = max(len(builder) for builder in builders_and_tests)
2181 for builder in sorted(builders_and_tests):
2182 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002183 return 0
2184
2185
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002186@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002188 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002189 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002190 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002191 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002193 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002194 if args:
2195 # One arg means set upstream branch.
2196 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2197 cl = Changelist()
2198 print "Upstream branch set to " + cl.GetUpstreamBranch()
2199 else:
2200 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201 return 0
2202
2203
thestig@chromium.org00858c82013-12-02 23:08:03 +00002204def CMDweb(parser, args):
2205 """Opens the current CL in the web browser."""
2206 _, args = parser.parse_args(args)
2207 if args:
2208 parser.error('Unrecognized args: %s' % ' '.join(args))
2209
2210 issue_url = Changelist().GetIssueURL()
2211 if not issue_url:
2212 print >> sys.stderr, 'ERROR No issue to open'
2213 return 1
2214
2215 webbrowser.open(issue_url)
2216 return 0
2217
2218
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002219def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002220 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002221 _, args = parser.parse_args(args)
2222 if args:
2223 parser.error('Unrecognized args: %s' % ' '.join(args))
2224 cl = Changelist()
2225 cl.SetFlag('commit', '1')
2226 return 0
2227
2228
groby@chromium.org411034a2013-02-26 15:12:01 +00002229def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002230 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002231 _, args = parser.parse_args(args)
2232 if args:
2233 parser.error('Unrecognized args: %s' % ' '.join(args))
2234 cl = Changelist()
2235 # Ensure there actually is an issue to close.
2236 cl.GetDescription()
2237 cl.CloseIssue()
2238 return 0
2239
2240
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002241def CMDdiff(parser, args):
2242 """shows differences between local tree and last upload."""
2243 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002244 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002245 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002246 if not issue:
2247 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002248 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002249 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002250
2251 # Create a new branch based on the merge-base
2252 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2253 try:
2254 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002255 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002256 if rtn != 0:
2257 return rtn
2258
2259 # Switch back to starting brand and diff against the temporary
2260 # branch containing the latest rietveld patch.
2261 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2262 finally:
2263 RunGit(['checkout', '-q', branch])
2264 RunGit(['branch', '-D', TMP_BRANCH])
2265
2266 return 0
2267
2268
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002269def CMDowners(parser, args):
2270 """interactively find the owners for reviewing"""
2271 parser.add_option(
2272 '--no-color',
2273 action='store_true',
2274 help='Use this option to disable color output')
2275 options, args = parser.parse_args(args)
2276
2277 author = RunGit(['config', 'user.email']).strip() or None
2278
2279 cl = Changelist()
2280
2281 if args:
2282 if len(args) > 1:
2283 parser.error('Unknown args')
2284 base_branch = args[0]
2285 else:
2286 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002287 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002288
2289 change = cl.GetChange(base_branch, None)
2290 return owners_finder.OwnersFinder(
2291 [f.LocalPath() for f in
2292 cl.GetChange(base_branch, None).AffectedFiles()],
2293 change.RepositoryRoot(), author,
2294 fopen=file, os_path=os.path, glob=glob.glob,
2295 disable_color=options.no_color).run()
2296
2297
enne@chromium.org555cfe42014-01-29 18:21:39 +00002298@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002299def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002300 """Runs clang-format on the diff."""
thakis@chromium.orge139d952014-02-02 19:38:08 +00002301 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002302 parser.add_option('--full', action='store_true',
2303 help='Reformat the full content of all touched files')
2304 parser.add_option('--dry-run', action='store_true',
2305 help='Don\'t modify any file on disk.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002306 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002307
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002308 # git diff generates paths against the root of the repository. Change
2309 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002310 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002311 if rel_base_path:
2312 os.chdir(rel_base_path)
2313
digit@chromium.org29e47272013-05-17 17:01:46 +00002314 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002315 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002316 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002317 # Only list the names of modified files.
2318 diff_cmd.append('--name-only')
2319 else:
2320 # Only generate context-less patches.
2321 diff_cmd.append('-U0')
2322
2323 # Grab the merge-base commit, i.e. the upstream commit of the current
2324 # branch when it was created or the last time it was rebased. This is
2325 # to cover the case where the user may have called "git fetch origin",
2326 # moving the origin branch to a newer commit, but hasn't rebased yet.
2327 upstream_commit = None
2328 cl = Changelist()
2329 upstream_branch = cl.GetUpstreamBranch()
2330 if upstream_branch:
2331 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2332 upstream_commit = upstream_commit.strip()
2333
2334 if not upstream_commit:
2335 DieWithError('Could not find base commit for this branch. '
2336 'Are you in detached state?')
2337
2338 diff_cmd.append(upstream_commit)
2339
2340 # Handle source file filtering.
2341 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002342 if args:
2343 for arg in args:
2344 if os.path.isdir(arg):
2345 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2346 elif os.path.isfile(arg):
2347 diff_cmd.append(arg)
2348 else:
2349 DieWithError('Argument "%s" is not a file or a directory' % arg)
2350 else:
2351 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002352 diff_output = RunGit(diff_cmd)
2353
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002354 top_dir = os.path.normpath(
2355 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2356
2357 # Locate the clang-format binary in the checkout
2358 try:
2359 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2360 except clang_format.NotFoundError, e:
2361 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002362
digit@chromium.org29e47272013-05-17 17:01:46 +00002363 if opts.full:
2364 # diff_output is a list of files to send to clang-format.
2365 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002366 if not files:
2367 print "Nothing to format."
2368 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002369 cmd = [clang_format_tool]
2370 if not opts.dry_run:
2371 cmd.append('-i')
2372 stdout = RunCommand(cmd + files, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002373 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002374 env = os.environ.copy()
2375 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002376 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002377 try:
2378 script = clang_format.FindClangFormatScriptInChromiumTree(
2379 'clang-format-diff.py')
2380 except clang_format.NotFoundError, e:
2381 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002382
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002383 cmd = [sys.executable, script, '-p0']
2384 if not opts.dry_run:
2385 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002386
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002387 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
2388 if opts.dry_run and len(stdout) > 0:
2389 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002390
2391 return 0
2392
2393
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002394class OptionParser(optparse.OptionParser):
2395 """Creates the option parse and add --verbose support."""
2396 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002397 optparse.OptionParser.__init__(
2398 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002399 self.add_option(
2400 '-v', '--verbose', action='count', default=0,
2401 help='Use 2 times for more debugging info')
2402
2403 def parse_args(self, args=None, values=None):
2404 options, args = optparse.OptionParser.parse_args(self, args, values)
2405 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2406 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2407 return options, args
2408
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002409
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002410def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002411 if sys.hexversion < 0x02060000:
2412 print >> sys.stderr, (
2413 '\nYour python version %s is unsupported, please upgrade.\n' %
2414 sys.version.split(' ', 1)[0])
2415 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002416
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002417 # Reload settings.
2418 global settings
2419 settings = Settings()
2420
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002421 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002422 dispatcher = subcommand.CommandDispatcher(__name__)
2423 try:
2424 return dispatcher.execute(OptionParser(), argv)
2425 except urllib2.HTTPError, e:
2426 if e.code != 500:
2427 raise
2428 DieWithError(
2429 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2430 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002431
2432
2433if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002434 # These affect sys.stdout so do it outside of main() to simplify mocks in
2435 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002436 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002437 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002438 sys.exit(main(sys.argv[1:]))