blob: 1d3d24cb85ad129204e8e6af9decfd2203868114 [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.org7a54e812014-02-11 19:57:22 +0000265 self.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.org7a54e812014-02-11 19:57:22 +0000306 @staticmethod
307 def GetRelativeRoot():
308 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000309
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000311 if self.root is None:
312 self.root = os.path.abspath(self.GetRelativeRoot())
313 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000314
315 def GetIsGitSvn(self):
316 """Return true if this repo looks like it's using git-svn."""
317 if self.is_git_svn is None:
318 # If you have any "svn-remote.*" config keys, we think you're using svn.
319 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000320 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321 return self.is_git_svn
322
323 def GetSVNBranch(self):
324 if self.svn_branch is None:
325 if not self.GetIsGitSvn():
326 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
327
328 # Try to figure out which remote branch we're based on.
329 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000330 # 1) iterate through our branch history and find the svn URL.
331 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000332
333 # regexp matching the git-svn line that contains the URL.
334 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
335
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000336 # We don't want to go through all of history, so read a line from the
337 # pipe at a time.
338 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000339 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000340 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
341 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000342 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000343 for line in proc.stdout:
344 match = git_svn_re.match(line)
345 if match:
346 url = match.group(1)
347 proc.stdout.close() # Cut pipe.
348 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000349
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000350 if url:
351 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
352 remotes = RunGit(['config', '--get-regexp',
353 r'^svn-remote\..*\.url']).splitlines()
354 for remote in remotes:
355 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000356 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000357 remote = match.group(1)
358 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000359 rewrite_root = RunGit(
360 ['config', 'svn-remote.%s.rewriteRoot' % remote],
361 error_ok=True).strip()
362 if rewrite_root:
363 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000364 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000365 ['config', 'svn-remote.%s.fetch' % remote],
366 error_ok=True).strip()
367 if fetch_spec:
368 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
369 if self.svn_branch:
370 break
371 branch_spec = RunGit(
372 ['config', 'svn-remote.%s.branches' % remote],
373 error_ok=True).strip()
374 if branch_spec:
375 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
376 if self.svn_branch:
377 break
378 tag_spec = RunGit(
379 ['config', 'svn-remote.%s.tags' % remote],
380 error_ok=True).strip()
381 if tag_spec:
382 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
383 if self.svn_branch:
384 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000385
386 if not self.svn_branch:
387 DieWithError('Can\'t guess svn branch -- try specifying it on the '
388 'command line')
389
390 return self.svn_branch
391
392 def GetTreeStatusUrl(self, error_ok=False):
393 if not self.tree_status_url:
394 error_message = ('You must configure your tree status URL by running '
395 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000396 self.tree_status_url = self._GetRietveldConfig(
397 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000398 return self.tree_status_url
399
400 def GetViewVCUrl(self):
401 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000402 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403 return self.viewvc_url
404
rmistry@google.com90752582014-01-14 21:04:50 +0000405 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000406 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000407
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000408 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000409 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000410
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000411 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000412 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000413
ukai@chromium.orge8077812012-02-03 03:41:46 +0000414 def GetIsGerrit(self):
415 """Return true if this repo is assosiated with gerrit code review system."""
416 if self.is_gerrit is None:
417 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
418 return self.is_gerrit
419
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000420 def GetGitEditor(self):
421 """Return the editor specified in the git config, or None if none is."""
422 if self.git_editor is None:
423 self.git_editor = self._GetConfig('core.editor', error_ok=True)
424 return self.git_editor or None
425
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000426 def _GetRietveldConfig(self, param, **kwargs):
427 return self._GetConfig('rietveld.' + param, **kwargs)
428
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000429 def _GetConfig(self, param, **kwargs):
430 self.LazyUpdateIfNeeded()
431 return RunGit(['config', param], **kwargs).strip()
432
433
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000434def ShortBranchName(branch):
435 """Convert a name like 'refs/heads/foo' to just 'foo'."""
436 return branch.replace('refs/heads/', '')
437
438
439class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000440 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000442 global settings
443 if not settings:
444 # Happens when git_cl.py is used as a utility library.
445 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 settings.GetDefaultServerUrl()
447 self.branchref = branchref
448 if self.branchref:
449 self.branch = ShortBranchName(self.branchref)
450 else:
451 self.branch = None
452 self.rietveld_server = None
453 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000454 self.lookedup_issue = False
455 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456 self.has_description = False
457 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000458 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000460 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000461 self.cc = None
462 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000463 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000464 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000465
466 def GetCCList(self):
467 """Return the users cc'd on this CL.
468
469 Return is a string suitable for passing to gcl with the --cc flag.
470 """
471 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000472 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000473 more_cc = ','.join(self.watchers)
474 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
475 return self.cc
476
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000477 def GetCCListWithoutDefault(self):
478 """Return the users cc'd on this CL excluding default ones."""
479 if self.cc is None:
480 self.cc = ','.join(self.watchers)
481 return self.cc
482
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000483 def SetWatchers(self, watchers):
484 """Set the list of email addresses that should be cc'd based on the changed
485 files in this CL.
486 """
487 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000488
489 def GetBranch(self):
490 """Returns the short branch name, e.g. 'master'."""
491 if not self.branch:
492 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
493 self.branch = ShortBranchName(self.branchref)
494 return self.branch
495
496 def GetBranchRef(self):
497 """Returns the full branch name, e.g. 'refs/heads/master'."""
498 self.GetBranch() # Poke the lazy loader.
499 return self.branchref
500
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000501 @staticmethod
502 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000503 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000504 e.g. 'origin', 'refs/heads/master'
505 """
506 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
508 error_ok=True).strip()
509 if upstream_branch:
510 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
511 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000512 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
513 error_ok=True).strip()
514 if upstream_branch:
515 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000516 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000517 # Fall back on trying a git-svn upstream branch.
518 if settings.GetIsGitSvn():
519 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000520 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000521 # Else, try to guess the origin remote.
522 remote_branches = RunGit(['branch', '-r']).split()
523 if 'origin/master' in remote_branches:
524 # Fall back on origin/master if it exits.
525 remote = 'origin'
526 upstream_branch = 'refs/heads/master'
527 elif 'origin/trunk' in remote_branches:
528 # Fall back on origin/trunk if it exists. Generally a shared
529 # git-svn clone
530 remote = 'origin'
531 upstream_branch = 'refs/heads/trunk'
532 else:
533 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000534Either pass complete "git diff"-style arguments, like
535 git cl upload origin/master
536or verify this branch is set up to track another (via the --track argument to
537"git checkout -b ...").""")
538
539 return remote, upstream_branch
540
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000541 def GetCommonAncestorWithUpstream(self):
542 return RunGit(['merge-base', self.GetUpstreamBranch(), 'HEAD']).strip()
543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000544 def GetUpstreamBranch(self):
545 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000546 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000547 if remote is not '.':
548 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
549 self.upstream_branch = upstream_branch
550 return self.upstream_branch
551
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000552 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000553 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000554 remote, branch = None, self.GetBranch()
555 seen_branches = set()
556 while branch not in seen_branches:
557 seen_branches.add(branch)
558 remote, branch = self.FetchUpstreamTuple(branch)
559 branch = ShortBranchName(branch)
560 if remote != '.' or branch.startswith('refs/remotes'):
561 break
562 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000563 remotes = RunGit(['remote'], error_ok=True).split()
564 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000565 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000566 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000567 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000568 logging.warning('Could not determine which remote this change is '
569 'associated with, so defaulting to "%s". This may '
570 'not be what you want. You may prevent this message '
571 'by running "git svn info" as documented here: %s',
572 self._remote,
573 GIT_INSTRUCTIONS_URL)
574 else:
575 logging.warn('Could not determine which remote this change is '
576 'associated with. You may prevent this message by '
577 'running "git svn info" as documented here: %s',
578 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000579 branch = 'HEAD'
580 if branch.startswith('refs/remotes'):
581 self._remote = (remote, branch)
582 else:
583 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000584 return self._remote
585
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000586 def GitSanityChecks(self, upstream_git_obj):
587 """Checks git repo status and ensures diff is from local commits."""
588
589 # Verify the commit we're diffing against is in our current branch.
590 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
591 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
592 if upstream_sha != common_ancestor:
593 print >> sys.stderr, (
594 'ERROR: %s is not in the current branch. You may need to rebase '
595 'your tracking branch' % upstream_sha)
596 return False
597
598 # List the commits inside the diff, and verify they are all local.
599 commits_in_diff = RunGit(
600 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
601 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
602 remote_branch = remote_branch.strip()
603 if code != 0:
604 _, remote_branch = self.GetRemoteBranch()
605
606 commits_in_remote = RunGit(
607 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
608
609 common_commits = set(commits_in_diff) & set(commits_in_remote)
610 if common_commits:
611 print >> sys.stderr, (
612 'ERROR: Your diff contains %d commits already in %s.\n'
613 'Run "git log --oneline %s..HEAD" to get a list of commits in '
614 'the diff. If you are using a custom git flow, you can override'
615 ' the reference used for this check with "git config '
616 'gitcl.remotebranch <git-ref>".' % (
617 len(common_commits), remote_branch, upstream_git_obj))
618 return False
619 return True
620
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000621 def GetGitBaseUrlFromConfig(self):
622 """Return the configured base URL from branch.<branchname>.baseurl.
623
624 Returns None if it is not set.
625 """
626 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
627 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRemoteUrl(self):
630 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
631
632 Returns None if there is no remote.
633 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000634 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
636
637 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000638 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000639 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000641 self.issue = int(issue) or None if issue else None
642 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 return self.issue
644
645 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000646 if not self.rietveld_server:
647 # If we're on a branch then get the server potentially associated
648 # with that branch.
649 if self.GetIssue():
650 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
651 ['config', self._RietveldServer()], error_ok=True).strip())
652 if not self.rietveld_server:
653 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000654 return self.rietveld_server
655
656 def GetIssueURL(self):
657 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000658 if not self.GetIssue():
659 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
661
662 def GetDescription(self, pretty=False):
663 if not self.has_description:
664 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000665 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000666 try:
667 self.description = self.RpcServer().get_description(issue).strip()
668 except urllib2.HTTPError, e:
669 if e.code == 404:
670 DieWithError(
671 ('\nWhile fetching the description for issue %d, received a '
672 '404 (not found)\n'
673 'error. It is likely that you deleted this '
674 'issue on the server. If this is the\n'
675 'case, please run\n\n'
676 ' git cl issue 0\n\n'
677 'to clear the association with the deleted issue. Then run '
678 'this command again.') % issue)
679 else:
680 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000681 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000682 self.has_description = True
683 if pretty:
684 wrapper = textwrap.TextWrapper()
685 wrapper.initial_indent = wrapper.subsequent_indent = ' '
686 return wrapper.fill(self.description)
687 return self.description
688
689 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000690 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000691 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 patchset = RunGit(['config', self._PatchsetSetting()],
693 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000694 self.patchset = int(patchset) or None if patchset else None
695 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 return self.patchset
697
698 def SetPatchset(self, patchset):
699 """Set this branch's patchset. If patchset=0, clears the patchset."""
700 if patchset:
701 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000702 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703 else:
704 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000705 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000706 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000708 def GetMostRecentPatchset(self):
709 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000710
711 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000712 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000713 '/download/issue%s_%s.diff' % (issue, patchset))
714
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000715 def GetIssueProperties(self):
716 if self._props is None:
717 issue = self.GetIssue()
718 if not issue:
719 self._props = {}
720 else:
721 self._props = self.RpcServer().get_issue_properties(issue, True)
722 return self._props
723
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000724 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000725 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000726
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727 def SetIssue(self, issue):
728 """Set this branch's issue. If issue=0, clears the issue."""
729 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000730 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 RunGit(['config', self._IssueSetting(), str(issue)])
732 if self.rietveld_server:
733 RunGit(['config', self._RietveldServer(), self.rietveld_server])
734 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000735 current_issue = self.GetIssue()
736 if current_issue:
737 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000738 self.issue = None
739 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000741 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000742 if not self.GitSanityChecks(upstream_branch):
743 DieWithError('\nGit sanity check failure')
744
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000745 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000746 if not root:
747 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000748 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000749
750 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000751 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000752 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000753 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000754 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000755 except subprocess2.CalledProcessError:
756 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000757 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000758 'This branch probably doesn\'t exist anymore. To reset the\n'
759 'tracking branch, please run\n'
760 ' git branch --set-upstream %s trunk\n'
761 'replacing trunk with origin/master or the relevant branch') %
762 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000763
maruel@chromium.org52424302012-08-29 15:14:30 +0000764 issue = self.GetIssue()
765 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000766 if issue:
767 description = self.GetDescription()
768 else:
769 # If the change was never uploaded, use the log messages of all commits
770 # up to the branch point, as git cl upload will prefill the description
771 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000772 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
773 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000774
775 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000776 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000777 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000778 name,
779 description,
780 absroot,
781 files,
782 issue,
783 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000784 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000785
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000786 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000787 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000788
789 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000790 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000791 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000792 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000793 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000794 except presubmit_support.PresubmitFailure, e:
795 DieWithError(
796 ('%s\nMaybe your depot_tools is out of date?\n'
797 'If all fails, contact maruel@') % e)
798
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000799 def UpdateDescription(self, description):
800 self.description = description
801 return self.RpcServer().update_description(
802 self.GetIssue(), self.description)
803
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000804 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000805 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000806 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000808 def SetFlag(self, flag, value):
809 """Patchset must match."""
810 if not self.GetPatchset():
811 DieWithError('The patchset needs to match. Send another patchset.')
812 try:
813 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000814 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000815 except urllib2.HTTPError, e:
816 if e.code == 404:
817 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
818 if e.code == 403:
819 DieWithError(
820 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
821 'match?') % (self.GetIssue(), self.GetPatchset()))
822 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000824 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 """Returns an upload.RpcServer() to access this review's rietveld instance.
826 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000827 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000828 self._rpc_server = rietveld.CachingRietveld(
829 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000830 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831
832 def _IssueSetting(self):
833 """Return the git setting that stores this change's issue."""
834 return 'branch.%s.rietveldissue' % self.GetBranch()
835
836 def _PatchsetSetting(self):
837 """Return the git setting that stores this change's most recent patchset."""
838 return 'branch.%s.rietveldpatchset' % self.GetBranch()
839
840 def _RietveldServer(self):
841 """Returns the git setting that stores this change's rietveld server."""
842 return 'branch.%s.rietveldserver' % self.GetBranch()
843
844
845def GetCodereviewSettingsInteractively():
846 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000847 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 server = settings.GetDefaultServerUrl(error_ok=True)
849 prompt = 'Rietveld server (host[:port])'
850 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000851 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852 if not server and not newserver:
853 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000854 if newserver:
855 newserver = gclient_utils.UpgradeToHttps(newserver)
856 if newserver != server:
857 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000859 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860 prompt = caption
861 if initial:
862 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000863 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864 if new_val == 'x':
865 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000866 elif new_val:
867 if is_url:
868 new_val = gclient_utils.UpgradeToHttps(new_val)
869 if new_val != initial:
870 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000871
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000872 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000873 SetProperty(settings.GetDefaultPrivateFlag(),
874 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000876 'tree-status-url', False)
877 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000878 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879
880 # TODO: configure a default branch to diff against, rather than this
881 # svn-based hackery.
882
883
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000884class ChangeDescription(object):
885 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000886 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000887 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000888
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000889 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000890 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000891
agable@chromium.org42c20792013-09-12 17:34:49 +0000892 @property # www.logilab.org/ticket/89786
893 def description(self): # pylint: disable=E0202
894 return '\n'.join(self._description_lines)
895
896 def set_description(self, desc):
897 if isinstance(desc, basestring):
898 lines = desc.splitlines()
899 else:
900 lines = [line.rstrip() for line in desc]
901 while lines and not lines[0]:
902 lines.pop(0)
903 while lines and not lines[-1]:
904 lines.pop(-1)
905 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000906
907 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000908 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000909 assert isinstance(reviewers, list), reviewers
910 if not reviewers:
911 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000912 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000913
agable@chromium.org42c20792013-09-12 17:34:49 +0000914 # Get the set of R= and TBR= lines and remove them from the desciption.
915 regexp = re.compile(self.R_LINE)
916 matches = [regexp.match(line) for line in self._description_lines]
917 new_desc = [l for i, l in enumerate(self._description_lines)
918 if not matches[i]]
919 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000920
agable@chromium.org42c20792013-09-12 17:34:49 +0000921 # Construct new unified R= and TBR= lines.
922 r_names = []
923 tbr_names = []
924 for match in matches:
925 if not match:
926 continue
927 people = cleanup_list([match.group(2).strip()])
928 if match.group(1) == 'TBR':
929 tbr_names.extend(people)
930 else:
931 r_names.extend(people)
932 for name in r_names:
933 if name not in reviewers:
934 reviewers.append(name)
935 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
936 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
937
938 # Put the new lines in the description where the old first R= line was.
939 line_loc = next((i for i, match in enumerate(matches) if match), -1)
940 if 0 <= line_loc < len(self._description_lines):
941 if new_tbr_line:
942 self._description_lines.insert(line_loc, new_tbr_line)
943 if new_r_line:
944 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000945 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000946 if new_r_line:
947 self.append_footer(new_r_line)
948 if new_tbr_line:
949 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000950
951 def prompt(self):
952 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000953 self.set_description([
954 '# Enter a description of the change.',
955 '# This will be displayed on the codereview site.',
956 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000957 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000958 '--------------------',
959 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000960
agable@chromium.org42c20792013-09-12 17:34:49 +0000961 regexp = re.compile(self.BUG_LINE)
962 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000963 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000964 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000965 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000966 if not content:
967 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000968 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000969
970 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000971 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
972 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000973 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000974 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000975
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000976 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000977 if self._description_lines:
978 # Add an empty line if either the last line or the new line isn't a tag.
979 last_line = self._description_lines[-1]
980 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
981 not presubmit_support.Change.TAG_LINE_RE.match(line)):
982 self._description_lines.append('')
983 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000984
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000985 def get_reviewers(self):
986 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000987 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
988 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000989 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000990
991
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000992def get_approving_reviewers(props):
993 """Retrieves the reviewers that approved a CL from the issue properties with
994 messages.
995
996 Note that the list may contain reviewers that are not committer, thus are not
997 considered by the CQ.
998 """
999 return sorted(
1000 set(
1001 message['sender']
1002 for message in props['messages']
1003 if message['approval'] and message['sender'] in props['reviewers']
1004 )
1005 )
1006
1007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008def FindCodereviewSettingsFile(filename='codereview.settings'):
1009 """Finds the given file starting in the cwd and going up.
1010
1011 Only looks up to the top of the repository unless an
1012 'inherit-review-settings-ok' file exists in the root of the repository.
1013 """
1014 inherit_ok_file = 'inherit-review-settings-ok'
1015 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001016 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1018 root = '/'
1019 while True:
1020 if filename in os.listdir(cwd):
1021 if os.path.isfile(os.path.join(cwd, filename)):
1022 return open(os.path.join(cwd, filename))
1023 if cwd == root:
1024 break
1025 cwd = os.path.dirname(cwd)
1026
1027
1028def LoadCodereviewSettingsFromFile(fileobj):
1029 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001030 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001032 def SetProperty(name, setting, unset_error_ok=False):
1033 fullname = 'rietveld.' + name
1034 if setting in keyvals:
1035 RunGit(['config', fullname, keyvals[setting]])
1036 else:
1037 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1038
1039 SetProperty('server', 'CODE_REVIEW_SERVER')
1040 # Only server setting is required. Other settings can be absent.
1041 # In that case, we ignore errors raised during option deletion attempt.
1042 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001043 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1045 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001046 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001048 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001049 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1052 #should be of the form
1053 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1054 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1055 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1056 keyvals['ORIGIN_URL_CONFIG']])
1057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001059def urlretrieve(source, destination):
1060 """urllib is broken for SSL connections via a proxy therefore we
1061 can't use urllib.urlretrieve()."""
1062 with open(destination, 'w') as f:
1063 f.write(urllib2.urlopen(source).read())
1064
1065
ukai@chromium.org712d6102013-11-27 00:52:58 +00001066def hasSheBang(fname):
1067 """Checks fname is a #! script."""
1068 with open(fname) as f:
1069 return f.read(2).startswith('#!')
1070
1071
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001072def DownloadHooks(force):
1073 """downloads hooks
1074
1075 Args:
1076 force: True to update hooks. False to install hooks if not present.
1077 """
1078 if not settings.GetIsGerrit():
1079 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001080 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001081 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1082 if not os.access(dst, os.X_OK):
1083 if os.path.exists(dst):
1084 if not force:
1085 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001086 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001087 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001088 if not hasSheBang(dst):
1089 DieWithError('Not a script: %s\n'
1090 'You need to download from\n%s\n'
1091 'into .git/hooks/commit-msg and '
1092 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001093 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1094 except Exception:
1095 if os.path.exists(dst):
1096 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001097 DieWithError('\nFailed to download hooks.\n'
1098 'You need to download from\n%s\n'
1099 'into .git/hooks/commit-msg and '
1100 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001101
1102
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001103@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001105 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001107 parser.add_option('--activate-update', action='store_true',
1108 help='activate auto-updating [rietveld] section in '
1109 '.git/config')
1110 parser.add_option('--deactivate-update', action='store_true',
1111 help='deactivate auto-updating [rietveld] section in '
1112 '.git/config')
1113 options, args = parser.parse_args(args)
1114
1115 if options.deactivate_update:
1116 RunGit(['config', 'rietveld.autoupdate', 'false'])
1117 return
1118
1119 if options.activate_update:
1120 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1121 return
1122
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 if len(args) == 0:
1124 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001125 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 return 0
1127
1128 url = args[0]
1129 if not url.endswith('codereview.settings'):
1130 url = os.path.join(url, 'codereview.settings')
1131
1132 # Load code review settings and download hooks (if available).
1133 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001134 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 return 0
1136
1137
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001138def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001139 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001140 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1141 branch = ShortBranchName(branchref)
1142 _, args = parser.parse_args(args)
1143 if not args:
1144 print("Current base-url:")
1145 return RunGit(['config', 'branch.%s.base-url' % branch],
1146 error_ok=False).strip()
1147 else:
1148 print("Setting base-url to %s" % args[0])
1149 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1150 error_ok=False).strip()
1151
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001154 """Show status of changelists.
1155
1156 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001157 - Red not sent for review or broken
1158 - Blue waiting for review
1159 - Yellow waiting for you to reply to review
1160 - Green LGTM'ed
1161 - Magenta in the commit queue
1162 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001163
1164 Also see 'git cl comments'.
1165 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 parser.add_option('--field',
1167 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001168 parser.add_option('-f', '--fast', action='store_true',
1169 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001171 if args:
1172 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001175 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 if options.field.startswith('desc'):
1177 print cl.GetDescription()
1178 elif options.field == 'id':
1179 issueid = cl.GetIssue()
1180 if issueid:
1181 print issueid
1182 elif options.field == 'patch':
1183 patchset = cl.GetPatchset()
1184 if patchset:
1185 print patchset
1186 elif options.field == 'url':
1187 url = cl.GetIssueURL()
1188 if url:
1189 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001190 return 0
1191
1192 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1193 if not branches:
1194 print('No local branch found.')
1195 return 0
1196
1197 changes = (Changelist(branchref=b) for b in branches.splitlines())
1198 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1199 alignment = max(5, max(len(b) for b in branches))
1200 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001201 # Adhoc thread pool to request data concurrently.
1202 output = Queue.Queue()
1203
1204 # Silence upload.py otherwise it becomes unweldly.
1205 upload.verbosity = 0
1206
1207 if not options.fast:
1208 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001209 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001210 c = Changelist(branchref=b)
1211 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001212 props = {}
1213 r = None
1214 if i:
1215 try:
1216 props = c.GetIssueProperties()
1217 r = c.GetApprovingReviewers() if i else None
1218 except urllib2.HTTPError:
1219 # The issue probably doesn't exist anymore.
1220 i += ' (broken)'
1221
1222 msgs = props.get('messages') or []
1223
1224 if not i:
1225 color = Fore.WHITE
1226 elif props.get('closed'):
1227 # Issue is closed.
1228 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001229 elif props.get('commit'):
1230 # Issue is in the commit queue.
1231 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001232 elif r:
1233 # Was LGTM'ed.
1234 color = Fore.GREEN
1235 elif not msgs:
1236 # No message was sent.
1237 color = Fore.RED
1238 elif msgs[-1]['sender'] != props.get('owner_email'):
1239 color = Fore.YELLOW
1240 else:
1241 color = Fore.BLUE
1242 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001243
1244 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1245 for t in threads:
1246 t.daemon = True
1247 t.start()
1248 else:
1249 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1250 for b in branches:
1251 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001252 url = c.GetIssueURL()
1253 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001254
1255 tmp = {}
1256 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001257 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001258 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001259 b, i, color = output.get()
1260 tmp[b] = (i, color)
1261 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001262 reset = Fore.RESET
1263 if not sys.stdout.isatty():
1264 color = ''
1265 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001266 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001267 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001268
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001269 cl = Changelist()
1270 print
1271 print 'Current branch:',
1272 if not cl.GetIssue():
1273 print 'no issue assigned.'
1274 return 0
1275 print cl.GetBranch()
1276 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1277 print 'Issue description:'
1278 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 return 0
1280
1281
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001282def colorize_CMDstatus_doc():
1283 """To be called once in main() to add colors to git cl status help."""
1284 colors = [i for i in dir(Fore) if i[0].isupper()]
1285
1286 def colorize_line(line):
1287 for color in colors:
1288 if color in line.upper():
1289 # Extract whitespaces first and the leading '-'.
1290 indent = len(line) - len(line.lstrip(' ')) + 1
1291 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1292 return line
1293
1294 lines = CMDstatus.__doc__.splitlines()
1295 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1296
1297
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001298@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001300 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301
1302 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001303 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001304 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305
1306 cl = Changelist()
1307 if len(args) > 0:
1308 try:
1309 issue = int(args[0])
1310 except ValueError:
1311 DieWithError('Pass a number to set the issue or none to list it.\n'
1312 'Maybe you want to run git cl status?')
1313 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001314 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315 return 0
1316
1317
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001318def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001319 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001320 (_, args) = parser.parse_args(args)
1321 if args:
1322 parser.error('Unsupported argument: %s' % args)
1323
1324 cl = Changelist()
1325 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001326 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001327 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001328 if message['disapproval']:
1329 color = Fore.RED
1330 elif message['approval']:
1331 color = Fore.GREEN
1332 elif message['sender'] == data['owner_email']:
1333 color = Fore.MAGENTA
1334 else:
1335 color = Fore.BLUE
1336 print '\n%s%s %s%s' % (
1337 color, message['date'].split('.', 1)[0], message['sender'],
1338 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001339 if message['text'].strip():
1340 print '\n'.join(' ' + l for l in message['text'].splitlines())
1341 return 0
1342
1343
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001344def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001345 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001346 cl = Changelist()
1347 if not cl.GetIssue():
1348 DieWithError('This branch has no associated changelist.')
1349 description = ChangeDescription(cl.GetDescription())
1350 description.prompt()
1351 cl.UpdateDescription(description.description)
1352 return 0
1353
1354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355def CreateDescriptionFromLog(args):
1356 """Pulls out the commit log to use as a base for the CL description."""
1357 log_args = []
1358 if len(args) == 1 and not args[0].endswith('.'):
1359 log_args = [args[0] + '..']
1360 elif len(args) == 1 and args[0].endswith('...'):
1361 log_args = [args[0][:-1]]
1362 elif len(args) == 2:
1363 log_args = [args[0] + '..' + args[1]]
1364 else:
1365 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001366 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367
1368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001370 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001371 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001373 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001374 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 (options, args) = parser.parse_args(args)
1376
ukai@chromium.org259e4682012-10-25 07:36:33 +00001377 if not options.force and is_dirty_git_tree('presubmit'):
1378 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 return 1
1380
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001381 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 if args:
1383 base_branch = args[0]
1384 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001385 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001386 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001388 cl.RunHook(
1389 committing=not options.upload,
1390 may_prompt=False,
1391 verbose=options.verbose,
1392 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001393 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394
1395
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001396def AddChangeIdToCommitMessage(options, args):
1397 """Re-commits using the current message, assumes the commit hook is in
1398 place.
1399 """
1400 log_desc = options.message or CreateDescriptionFromLog(args)
1401 git_command = ['commit', '--amend', '-m', log_desc]
1402 RunGit(git_command)
1403 new_log_desc = CreateDescriptionFromLog(args)
1404 if CHANGE_ID in new_log_desc:
1405 print 'git-cl: Added Change-Id to commit message.'
1406 else:
1407 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1408
1409
ukai@chromium.orge8077812012-02-03 03:41:46 +00001410def GerritUpload(options, args, cl):
1411 """upload the current branch to gerrit."""
1412 # We assume the remote called "origin" is the one we want.
1413 # It is probably not worthwhile to support different workflows.
1414 remote = 'origin'
1415 branch = 'master'
1416 if options.target_branch:
1417 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001419 change_desc = ChangeDescription(
1420 options.message or CreateDescriptionFromLog(args))
1421 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001422 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001424 if CHANGE_ID not in change_desc.description:
1425 AddChangeIdToCommitMessage(options, args)
1426 if options.reviewers:
1427 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428
ukai@chromium.orge8077812012-02-03 03:41:46 +00001429 receive_options = []
1430 cc = cl.GetCCList().split(',')
1431 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001432 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001433 cc = filter(None, cc)
1434 if cc:
1435 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001436 if change_desc.get_reviewers():
1437 receive_options.extend(
1438 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439
ukai@chromium.orge8077812012-02-03 03:41:46 +00001440 git_command = ['push']
1441 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001442 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001443 ' '.join(receive_options))
1444 git_command += [remote, 'HEAD:refs/for/' + branch]
1445 RunGit(git_command)
1446 # TODO(ukai): parse Change-Id: and set issue number?
1447 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001448
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449
ukai@chromium.orge8077812012-02-03 03:41:46 +00001450def RietveldUpload(options, args, cl):
1451 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1453 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 if options.emulate_svn_auto_props:
1455 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456
1457 change_desc = None
1458
pgervais@chromium.org91141372014-01-09 23:27:20 +00001459 if options.email is not None:
1460 upload_args.extend(['--email', options.email])
1461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001463 if options.title:
1464 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001465 if options.message:
1466 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001467 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 print ("This branch is associated with issue %s. "
1469 "Adding patch to that issue." % cl.GetIssue())
1470 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001471 if options.title:
1472 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001473 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001474 change_desc = ChangeDescription(message)
1475 if options.reviewers:
1476 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001477 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001478 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001479
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001480 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481 print "Description is empty; aborting."
1482 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001483
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001484 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001485 if change_desc.get_reviewers():
1486 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001487 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001488 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001489 DieWithError("Must specify reviewers to send email.")
1490 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001491
1492 # We check this before applying rietveld.private assuming that in
1493 # rietveld.cc only addresses which we can send private CLs to are listed
1494 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1495 # --private is specified explicitly on the command line.
1496 if options.private:
1497 logging.warn('rietveld.cc is ignored since private flag is specified. '
1498 'You need to review and add them manually if necessary.')
1499 cc = cl.GetCCListWithoutDefault()
1500 else:
1501 cc = cl.GetCCList()
1502 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001503 if cc:
1504 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001506 if options.private or settings.GetDefaultPrivateFlag() == "True":
1507 upload_args.append('--private')
1508
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001509 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001510 if not options.find_copies:
1511 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001512
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513 # Include the upstream repo's URL in the change -- this is useful for
1514 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001515 remote_url = cl.GetGitBaseUrlFromConfig()
1516 if not remote_url:
1517 if settings.GetIsGitSvn():
1518 # URL is dependent on the current directory.
1519 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1520 if data:
1521 keys = dict(line.split(': ', 1) for line in data.splitlines()
1522 if ': ' in line)
1523 remote_url = keys.get('URL', None)
1524 else:
1525 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1526 remote_url = (cl.GetRemoteUrl() + '@'
1527 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001528 if remote_url:
1529 upload_args.extend(['--base_url', remote_url])
1530
1531 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001532 upload_args = ['upload'] + upload_args + args
1533 logging.info('upload.RealMain(%s)', upload_args)
1534 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001535 issue = int(issue)
1536 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001537 except KeyboardInterrupt:
1538 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001539 except:
1540 # If we got an exception after the user typed a description for their
1541 # change, back up the description before re-raising.
1542 if change_desc:
1543 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1544 print '\nGot exception while uploading -- saving description to %s\n' \
1545 % backup_path
1546 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001547 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548 backup_file.close()
1549 raise
1550
1551 if not cl.GetIssue():
1552 cl.SetIssue(issue)
1553 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001554
1555 if options.use_commit_queue:
1556 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557 return 0
1558
1559
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001560def cleanup_list(l):
1561 """Fixes a list so that comma separated items are put as individual items.
1562
1563 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1564 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1565 """
1566 items = sum((i.split(',') for i in l), [])
1567 stripped_items = (i.strip() for i in items)
1568 return sorted(filter(None, stripped_items))
1569
1570
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001571@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001572def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001573 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001574 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1575 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001576 parser.add_option('--bypass-watchlists', action='store_true',
1577 dest='bypass_watchlists',
1578 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001579 parser.add_option('-f', action='store_true', dest='force',
1580 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001581 parser.add_option('-m', dest='message', help='message for patchset')
1582 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001583 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001584 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001585 help='reviewer email addresses')
1586 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001587 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001588 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001589 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001590 help='send email to reviewer immediately')
1591 parser.add_option("--emulate_svn_auto_props", action="store_true",
1592 dest="emulate_svn_auto_props",
1593 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001594 parser.add_option('-c', '--use-commit-queue', action='store_true',
1595 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001596 parser.add_option('--private', action='store_true',
1597 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001598 parser.add_option('--target_branch',
1599 help='When uploading to gerrit, remote branch to '
1600 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001601 parser.add_option('--email', default=None,
1602 help='email address to use to connect to Rietveld')
1603
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001604 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001605 (options, args) = parser.parse_args(args)
1606
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001607 if options.target_branch and not settings.GetIsGerrit():
1608 parser.error('Use --target_branch for non gerrit repository.')
1609
ukai@chromium.org259e4682012-10-25 07:36:33 +00001610 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001611 return 1
1612
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001613 options.reviewers = cleanup_list(options.reviewers)
1614 options.cc = cleanup_list(options.cc)
1615
ukai@chromium.orge8077812012-02-03 03:41:46 +00001616 cl = Changelist()
1617 if args:
1618 # TODO(ukai): is it ok for gerrit case?
1619 base_branch = args[0]
1620 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001621 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001622 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001623 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001624
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001625 # Apply watchlists on upload.
1626 change = cl.GetChange(base_branch, None)
1627 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1628 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001629 if not options.bypass_watchlists:
1630 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001631
ukai@chromium.orge8077812012-02-03 03:41:46 +00001632 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001633 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001634 may_prompt=not options.force,
1635 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001636 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001637 if not hook_results.should_continue():
1638 return 1
1639 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001640 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001641
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001642 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001643 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001644 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001645 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001646 print ('The last upload made from this repository was patchset #%d but '
1647 'the most recent patchset on the server is #%d.'
1648 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001649 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1650 'from another machine or branch the patch you\'re uploading now '
1651 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001652 ask_for_data('About to upload; enter to confirm.')
1653
iannucci@chromium.org79540052012-10-19 23:15:26 +00001654 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001655 if settings.GetIsGerrit():
1656 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001657 ret = RietveldUpload(options, args, cl)
1658 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001659 git_set_branch_value('last-upload-hash',
1660 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001661
1662 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001663
1664
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001665def IsSubmoduleMergeCommit(ref):
1666 # When submodules are added to the repo, we expect there to be a single
1667 # non-git-svn merge commit at remote HEAD with a signature comment.
1668 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001669 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001670 return RunGit(cmd) != ''
1671
1672
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673def SendUpstream(parser, args, cmd):
1674 """Common code for CmdPush and CmdDCommit
1675
1676 Squashed commit into a single.
1677 Updates changelog with metadata (e.g. pointer to review).
1678 Pushes/dcommits the code upstream.
1679 Updates review and closes.
1680 """
1681 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1682 help='bypass upload presubmit hook')
1683 parser.add_option('-m', dest='message',
1684 help="override review description")
1685 parser.add_option('-f', action='store_true', dest='force',
1686 help="force yes to questions (don't prompt)")
1687 parser.add_option('-c', dest='contributor',
1688 help="external contributor for patch (appended to " +
1689 "description and used as author for git). Should be " +
1690 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001691 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692 (options, args) = parser.parse_args(args)
1693 cl = Changelist()
1694
1695 if not args or cmd == 'push':
1696 # Default to merging against our best guess of the upstream branch.
1697 args = [cl.GetUpstreamBranch()]
1698
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001699 if options.contributor:
1700 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1701 print "Please provide contibutor as 'First Last <email@example.com>'"
1702 return 1
1703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001705 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706
ukai@chromium.org259e4682012-10-25 07:36:33 +00001707 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708 return 1
1709
1710 # This rev-list syntax means "show all commits not in my branch that
1711 # are in base_branch".
1712 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1713 base_branch]).splitlines()
1714 if upstream_commits:
1715 print ('Base branch "%s" has %d commits '
1716 'not in this branch.' % (base_branch, len(upstream_commits)))
1717 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1718 return 1
1719
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001720 # This is the revision `svn dcommit` will commit on top of.
1721 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1722 '--pretty=format:%H'])
1723
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001725 # If the base_head is a submodule merge commit, the first parent of the
1726 # base_head should be a git-svn commit, which is what we're interested in.
1727 base_svn_head = base_branch
1728 if base_has_submodules:
1729 base_svn_head += '^1'
1730
1731 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732 if extra_commits:
1733 print ('This branch has %d additional commits not upstreamed yet.'
1734 % len(extra_commits.splitlines()))
1735 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1736 'before attempting to %s.' % (base_branch, cmd))
1737 return 1
1738
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001739 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001740 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001741 author = None
1742 if options.contributor:
1743 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001744 hook_results = cl.RunHook(
1745 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001746 may_prompt=not options.force,
1747 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001748 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001749 if not hook_results.should_continue():
1750 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751
1752 if cmd == 'dcommit':
1753 # Check the tree status if the tree status URL is set.
1754 status = GetTreeStatus()
1755 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001756 print('The tree is closed. Please wait for it to reopen. Use '
1757 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001758 return 1
1759 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001760 print('Unable to determine tree status. Please verify manually and '
1761 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001762 else:
1763 breakpad.SendStack(
1764 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001765 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1766 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001767 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001769 change_desc = ChangeDescription(options.message)
1770 if not change_desc.description and cl.GetIssue():
1771 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001773 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001774 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001775 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001776 else:
1777 print 'No description set.'
1778 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1779 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001781 # Keep a separate copy for the commit message, because the commit message
1782 # contains the link to the Rietveld issue, while the Rietveld message contains
1783 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001784 # Keep a separate copy for the commit message.
1785 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001786 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001787
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001788 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001789 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001790 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001792 commit_desc.append_footer('Patch from %s.' % options.contributor)
1793
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001794 print('Description:')
1795 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796
1797 branches = [base_branch, cl.GetBranchRef()]
1798 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001799 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001800 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001802 # We want to squash all this branch's commits into one commit with the proper
1803 # description. We do this by doing a "reset --soft" to the base branch (which
1804 # keeps the working copy the same), then dcommitting that. If origin/master
1805 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1806 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001807 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001808 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1809 # Delete the branches if they exist.
1810 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1811 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1812 result = RunGitWithCode(showref_cmd)
1813 if result[0] == 0:
1814 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815
1816 # We might be in a directory that's present in this branch but not in the
1817 # trunk. Move up to the top of the tree so that git commands that expect a
1818 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001819 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820 if rel_base_path:
1821 os.chdir(rel_base_path)
1822
1823 # Stuff our change into the merge branch.
1824 # We wrap in a try...finally block so if anything goes wrong,
1825 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001826 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001828 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1829 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001831 RunGit(
1832 [
1833 'commit', '--author', options.contributor,
1834 '-m', commit_desc.description,
1835 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001837 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001838 if base_has_submodules:
1839 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1840 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1841 RunGit(['checkout', CHERRY_PICK_BRANCH])
1842 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 if cmd == 'push':
1844 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001845 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 retcode, output = RunGitWithCode(
1847 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1848 logging.debug(output)
1849 else:
1850 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001851 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001852 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001853 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854 finally:
1855 # And then swap back to the original branch and clean up.
1856 RunGit(['checkout', '-q', cl.GetBranch()])
1857 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001858 if base_has_submodules:
1859 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860
1861 if cl.GetIssue():
1862 if cmd == 'dcommit' and 'Committed r' in output:
1863 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1864 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001865 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1866 for l in output.splitlines(False))
1867 match = filter(None, match)
1868 if len(match) != 1:
1869 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1870 output)
1871 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872 else:
1873 return 1
1874 viewvc_url = settings.GetViewVCUrl()
1875 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001876 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001877 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001878 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001879 print ('Closing issue '
1880 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001881 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001882 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001883 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001884 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001885 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001886 if options.bypass_hooks:
1887 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1888 else:
1889 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001890 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001891 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001892
1893 if retcode == 0:
1894 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1895 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001896 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001897
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898 return 0
1899
1900
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001901@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001903 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001904 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001905 message = """This doesn't appear to be an SVN repository.
1906If your project has a git mirror with an upstream SVN master, you probably need
1907to run 'git svn init', see your project's git mirror documentation.
1908If your project has a true writeable upstream repository, you probably want
1909to run 'git cl push' instead.
1910Choose wisely, if you get this wrong, your commit might appear to succeed but
1911will instead be silently ignored."""
1912 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001913 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001914 return SendUpstream(parser, args, 'dcommit')
1915
1916
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001917@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001919 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920 if settings.GetIsGitSvn():
1921 print('This appears to be an SVN repository.')
1922 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001923 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924 return SendUpstream(parser, args, 'push')
1925
1926
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001927@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001929 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930 parser.add_option('-b', dest='newbranch',
1931 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001932 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001934 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1935 help='Change to the directory DIR immediately, '
1936 'before doing anything else.')
1937 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001938 help='failed patches spew .rej files rather than '
1939 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1941 help="don't commit after patch applies")
1942 (options, args) = parser.parse_args(args)
1943 if len(args) != 1:
1944 parser.print_help()
1945 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001946 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001948 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001949 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001950
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001951 if options.newbranch:
1952 if options.force:
1953 RunGit(['branch', '-D', options.newbranch],
1954 stderr=subprocess2.PIPE, error_ok=True)
1955 RunGit(['checkout', '-b', options.newbranch,
1956 Changelist().GetUpstreamBranch()])
1957
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001958 return PatchIssue(issue_arg, options.reject, options.nocommit,
1959 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001960
1961
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001962def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001963 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001965 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001966 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001967 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001968 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001969 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001970 # Assume it's a URL to the patch. Default to https.
1971 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001972 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001973 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 DieWithError('Must pass an issue ID or full URL for '
1975 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001976 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001977 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001978 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001980 # Switch up to the top-level directory, if necessary, in preparation for
1981 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001982 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983 if top:
1984 os.chdir(top)
1985
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986 # Git patches have a/ at the beginning of source paths. We strip that out
1987 # with a sed script rather than the -p flag to patch so we can feed either
1988 # Git or svn-style patches into the same apply command.
1989 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001990 try:
1991 patch_data = subprocess2.check_output(
1992 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1993 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994 DieWithError('Git patch mungling failed.')
1995 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001996
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997 # We use "git apply" to apply the patch instead of "patch" so that we can
1998 # pick up file adds.
1999 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002000 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002001 if directory:
2002 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002003 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002005 elif IsGitVersionAtLeast('1.7.12'):
2006 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002007 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002008 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002009 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002010 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 DieWithError('Failed to apply the patch')
2012
2013 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002014 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2016 cl = Changelist()
2017 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002018 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002019 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020 else:
2021 print "Patch applied to index."
2022 return 0
2023
2024
2025def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002026 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002027 # Provide a wrapper for git svn rebase to help avoid accidental
2028 # git svn dcommit.
2029 # It's the only command that doesn't use parser at all since we just defer
2030 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002031
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002032 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033
2034
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002035def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036 """Fetches the tree status and returns either 'open', 'closed',
2037 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002038 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039 if url:
2040 status = urllib2.urlopen(url).read().lower()
2041 if status.find('closed') != -1 or status == '0':
2042 return 'closed'
2043 elif status.find('open') != -1 or status == '1':
2044 return 'open'
2045 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046 return 'unset'
2047
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049def GetTreeStatusReason():
2050 """Fetches the tree status from a json url and returns the message
2051 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002052 url = settings.GetTreeStatusUrl()
2053 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054 connection = urllib2.urlopen(json_url)
2055 status = json.loads(connection.read())
2056 connection.close()
2057 return status['message']
2058
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002061 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002062 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063 status = GetTreeStatus()
2064 if 'unset' == status:
2065 print 'You must configure your tree status URL by running "git cl config".'
2066 return 2
2067
2068 print "The tree is %s" % status
2069 print
2070 print GetTreeStatusReason()
2071 if status != 'open':
2072 return 1
2073 return 0
2074
2075
maruel@chromium.org15192402012-09-06 12:38:29 +00002076def CMDtry(parser, args):
2077 """Triggers a try job through Rietveld."""
2078 group = optparse.OptionGroup(parser, "Try job options")
2079 group.add_option(
2080 "-b", "--bot", action="append",
2081 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2082 "times to specify multiple builders. ex: "
2083 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2084 "the try server waterfall for the builders name and the tests "
2085 "available. Can also be used to specify gtest_filter, e.g. "
2086 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2087 group.add_option(
2088 "-r", "--revision",
2089 help="Revision to use for the try job; default: the "
2090 "revision will be determined by the try server; see "
2091 "its waterfall for more info")
2092 group.add_option(
2093 "-c", "--clobber", action="store_true", default=False,
2094 help="Force a clobber before building; e.g. don't do an "
2095 "incremental build")
2096 group.add_option(
2097 "--project",
2098 help="Override which project to use. Projects are defined "
2099 "server-side to define what default bot set to use")
2100 group.add_option(
2101 "-t", "--testfilter", action="append", default=[],
2102 help=("Apply a testfilter to all the selected builders. Unless the "
2103 "builders configurations are similar, use multiple "
2104 "--bot <builder>:<test> arguments."))
2105 group.add_option(
2106 "-n", "--name", help="Try job name; default to current branch name")
2107 parser.add_option_group(group)
2108 options, args = parser.parse_args(args)
2109
2110 if args:
2111 parser.error('Unknown arguments: %s' % args)
2112
2113 cl = Changelist()
2114 if not cl.GetIssue():
2115 parser.error('Need to upload first')
2116
2117 if not options.name:
2118 options.name = cl.GetBranch()
2119
2120 # Process --bot and --testfilter.
2121 if not options.bot:
2122 # Get try slaves from PRESUBMIT.py files if not specified.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002123 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002124 options.bot = presubmit_support.DoGetTrySlaves(
2125 change,
2126 change.LocalPaths(),
2127 settings.GetRoot(),
2128 None,
2129 None,
2130 options.verbose,
2131 sys.stdout)
2132 if not options.bot:
2133 parser.error('No default try builder to try, use --bot')
2134
2135 builders_and_tests = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002136 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2137 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2138
2139 for bot in old_style:
maruel@chromium.org15192402012-09-06 12:38:29 +00002140 if ':' in bot:
2141 builder, tests = bot.split(':', 1)
2142 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2143 elif ',' in bot:
2144 parser.error('Specify one bot per --bot flag')
2145 else:
2146 builders_and_tests.setdefault(bot, []).append('defaulttests')
2147
stip@chromium.org43064fd2013-12-18 20:07:44 +00002148 for bot, tests in new_style:
2149 builders_and_tests.setdefault(bot, []).extend(tests)
2150
maruel@chromium.org15192402012-09-06 12:38:29 +00002151 if options.testfilter:
2152 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2153 builders_and_tests = dict(
2154 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2155 if t != ['compile'])
2156
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002157 if any('triggered' in b for b in builders_and_tests):
2158 print >> sys.stderr, (
2159 'ERROR You are trying to send a job to a triggered bot. This type of'
2160 ' bot requires an\ninitial job from a parent (usually a builder). '
2161 'Instead send your job to the parent.\n'
2162 'Bot list: %s' % builders_and_tests)
2163 return 1
2164
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002165 patchset = cl.GetMostRecentPatchset()
2166 if patchset and patchset != cl.GetPatchset():
2167 print(
2168 '\nWARNING Mismatch between local config and server. Did a previous '
2169 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2170 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002171 try:
2172 cl.RpcServer().trigger_try_jobs(
2173 cl.GetIssue(), patchset, options.name, options.clobber,
2174 options.revision, builders_and_tests)
2175 except urllib2.HTTPError, e:
2176 if e.code == 404:
2177 print('404 from rietveld; '
2178 'did you mean to use "git try" instead of "git cl try"?')
2179 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002180 print('Tried jobs on:')
2181 length = max(len(builder) for builder in builders_and_tests)
2182 for builder in sorted(builders_and_tests):
2183 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002184 return 0
2185
2186
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002187@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002188def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002189 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002190 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002191 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002192 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002193
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002194 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002195 if args:
2196 # One arg means set upstream branch.
2197 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2198 cl = Changelist()
2199 print "Upstream branch set to " + cl.GetUpstreamBranch()
2200 else:
2201 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002202 return 0
2203
2204
thestig@chromium.org00858c82013-12-02 23:08:03 +00002205def CMDweb(parser, args):
2206 """Opens the current CL in the web browser."""
2207 _, args = parser.parse_args(args)
2208 if args:
2209 parser.error('Unrecognized args: %s' % ' '.join(args))
2210
2211 issue_url = Changelist().GetIssueURL()
2212 if not issue_url:
2213 print >> sys.stderr, 'ERROR No issue to open'
2214 return 1
2215
2216 webbrowser.open(issue_url)
2217 return 0
2218
2219
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002220def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002221 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002222 _, args = parser.parse_args(args)
2223 if args:
2224 parser.error('Unrecognized args: %s' % ' '.join(args))
2225 cl = Changelist()
2226 cl.SetFlag('commit', '1')
2227 return 0
2228
2229
groby@chromium.org411034a2013-02-26 15:12:01 +00002230def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002231 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002232 _, args = parser.parse_args(args)
2233 if args:
2234 parser.error('Unrecognized args: %s' % ' '.join(args))
2235 cl = Changelist()
2236 # Ensure there actually is an issue to close.
2237 cl.GetDescription()
2238 cl.CloseIssue()
2239 return 0
2240
2241
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002242def CMDdiff(parser, args):
2243 """shows differences between local tree and last upload."""
2244 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002245 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002246 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002247 if not issue:
2248 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002249 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002250 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002251
2252 # Create a new branch based on the merge-base
2253 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2254 try:
2255 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002256 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002257 if rtn != 0:
2258 return rtn
2259
2260 # Switch back to starting brand and diff against the temporary
2261 # branch containing the latest rietveld patch.
2262 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2263 finally:
2264 RunGit(['checkout', '-q', branch])
2265 RunGit(['branch', '-D', TMP_BRANCH])
2266
2267 return 0
2268
2269
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002270def CMDowners(parser, args):
2271 """interactively find the owners for reviewing"""
2272 parser.add_option(
2273 '--no-color',
2274 action='store_true',
2275 help='Use this option to disable color output')
2276 options, args = parser.parse_args(args)
2277
2278 author = RunGit(['config', 'user.email']).strip() or None
2279
2280 cl = Changelist()
2281
2282 if args:
2283 if len(args) > 1:
2284 parser.error('Unknown args')
2285 base_branch = args[0]
2286 else:
2287 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002288 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002289
2290 change = cl.GetChange(base_branch, None)
2291 return owners_finder.OwnersFinder(
2292 [f.LocalPath() for f in
2293 cl.GetChange(base_branch, None).AffectedFiles()],
2294 change.RepositoryRoot(), author,
2295 fopen=file, os_path=os.path, glob=glob.glob,
2296 disable_color=options.no_color).run()
2297
2298
enne@chromium.org555cfe42014-01-29 18:21:39 +00002299@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002300def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002301 """Runs clang-format on the diff."""
thakis@chromium.orge139d952014-02-02 19:38:08 +00002302 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002303 parser.add_option('--full', action='store_true',
2304 help='Reformat the full content of all touched files')
2305 parser.add_option('--dry-run', action='store_true',
2306 help='Don\'t modify any file on disk.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002307 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002308
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002309 # git diff generates paths against the root of the repository. Change
2310 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002311 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002312 if rel_base_path:
2313 os.chdir(rel_base_path)
2314
digit@chromium.org29e47272013-05-17 17:01:46 +00002315 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002316 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002317 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002318 # Only list the names of modified files.
2319 diff_cmd.append('--name-only')
2320 else:
2321 # Only generate context-less patches.
2322 diff_cmd.append('-U0')
2323
2324 # Grab the merge-base commit, i.e. the upstream commit of the current
2325 # branch when it was created or the last time it was rebased. This is
2326 # to cover the case where the user may have called "git fetch origin",
2327 # moving the origin branch to a newer commit, but hasn't rebased yet.
2328 upstream_commit = None
2329 cl = Changelist()
2330 upstream_branch = cl.GetUpstreamBranch()
2331 if upstream_branch:
2332 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2333 upstream_commit = upstream_commit.strip()
2334
2335 if not upstream_commit:
2336 DieWithError('Could not find base commit for this branch. '
2337 'Are you in detached state?')
2338
2339 diff_cmd.append(upstream_commit)
2340
2341 # Handle source file filtering.
2342 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002343 if args:
2344 for arg in args:
2345 if os.path.isdir(arg):
2346 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2347 elif os.path.isfile(arg):
2348 diff_cmd.append(arg)
2349 else:
2350 DieWithError('Argument "%s" is not a file or a directory' % arg)
2351 else:
2352 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002353 diff_output = RunGit(diff_cmd)
2354
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002355 top_dir = os.path.normpath(
2356 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2357
2358 # Locate the clang-format binary in the checkout
2359 try:
2360 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2361 except clang_format.NotFoundError, e:
2362 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002363
digit@chromium.org29e47272013-05-17 17:01:46 +00002364 if opts.full:
2365 # diff_output is a list of files to send to clang-format.
2366 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002367 if not files:
2368 print "Nothing to format."
2369 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002370 cmd = [clang_format_tool]
2371 if not opts.dry_run:
2372 cmd.append('-i')
2373 stdout = RunCommand(cmd + files, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002374 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002375 env = os.environ.copy()
2376 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002377 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002378 try:
2379 script = clang_format.FindClangFormatScriptInChromiumTree(
2380 'clang-format-diff.py')
2381 except clang_format.NotFoundError, e:
2382 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002383
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002384 cmd = [sys.executable, script, '-p0']
2385 if not opts.dry_run:
2386 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002387
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002388 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
2389 if opts.dry_run and len(stdout) > 0:
2390 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002391
2392 return 0
2393
2394
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002395class OptionParser(optparse.OptionParser):
2396 """Creates the option parse and add --verbose support."""
2397 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002398 optparse.OptionParser.__init__(
2399 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002400 self.add_option(
2401 '-v', '--verbose', action='count', default=0,
2402 help='Use 2 times for more debugging info')
2403
2404 def parse_args(self, args=None, values=None):
2405 options, args = optparse.OptionParser.parse_args(self, args, values)
2406 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2407 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2408 return options, args
2409
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002411def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002412 if sys.hexversion < 0x02060000:
2413 print >> sys.stderr, (
2414 '\nYour python version %s is unsupported, please upgrade.\n' %
2415 sys.version.split(' ', 1)[0])
2416 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002417
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002418 # Reload settings.
2419 global settings
2420 settings = Settings()
2421
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002422 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002423 dispatcher = subcommand.CommandDispatcher(__name__)
2424 try:
2425 return dispatcher.execute(OptionParser(), argv)
2426 except urllib2.HTTPError, e:
2427 if e.code != 500:
2428 raise
2429 DieWithError(
2430 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2431 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002432
2433
2434if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002435 # These affect sys.stdout so do it outside of main() to simplify mocks in
2436 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002437 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002438 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002439 sys.exit(main(sys.argv[1:]))