blob: 6e564010d1a47e59752891611b37fbce551a643b [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
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000010import datetime
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000013import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import logging
15import optparse
16import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000017import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import textwrap
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000022import time
maruel@chromium.org1033efd2013-07-23 23:25:09 +000023import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000025import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000026import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027
28try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000029 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030except ImportError:
31 pass
32
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035from third_party import upload
36import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
41import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000043import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000046import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047import watchlists
48
maruel@chromium.org0633fb42013-08-16 20:06:14 +000049__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000051DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000052POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000054GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000055CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056
thestig@chromium.org44202a22014-03-11 19:22:18 +000057# Valid extensions for files we want to lint.
58DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
59DEFAULT_LINT_IGNORE_REGEX = r"$^"
60
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000061# Shortcut since it quickly becomes redundant.
62Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000063
maruel@chromium.orgddd59412011-11-30 14:20:38 +000064# Initialized in main()
65settings = None
66
67
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000069 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070 sys.exit(1)
71
72
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000073def GetNoGitPagerEnv():
74 env = os.environ.copy()
75 # 'cat' is a magical git string that disables pagers on all platforms.
76 env['GIT_PAGER'] = 'cat'
77 return env
78
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000081 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000082 except subprocess2.CalledProcessError as e:
83 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000086 'Command "%s" failed.\n%s' % (
87 ' '.join(args), error_message or e.stdout or ''))
88 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089
90
91def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000092 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000093 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000094
95
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000096def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000098 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000099 if suppress_stderr:
100 stderr = subprocess2.VOID
101 else:
102 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000103 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000104 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000105 stdout=subprocess2.PIPE,
106 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000107 return code, out[0]
108 except ValueError:
109 # When the subprocess fails, it returns None. That triggers a ValueError
110 # when trying to unpack the return value into (out, code).
111 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000114def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000115 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000117 return (version.startswith(prefix) and
118 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000119
120
maruel@chromium.org90541732011-04-01 17:54:18 +0000121def ask_for_data(prompt):
122 try:
123 return raw_input(prompt)
124 except KeyboardInterrupt:
125 # Hide the exception.
126 sys.exit(1)
127
128
iannucci@chromium.org79540052012-10-19 23:15:26 +0000129def git_set_branch_value(key, value):
130 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000131 if not branch:
132 return
133
134 cmd = ['config']
135 if isinstance(value, int):
136 cmd.append('--int')
137 git_key = 'branch.%s.%s' % (branch, key)
138 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139
140
141def git_get_branch_default(key, default):
142 branch = Changelist().GetBranch()
143 if branch:
144 git_key = 'branch.%s.%s' % (branch, key)
145 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
146 try:
147 return int(stdout.strip())
148 except ValueError:
149 pass
150 return default
151
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153def add_git_similarity(parser):
154 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 help='Sets the percentage that a pair of files need to match in order to'
157 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158 parser.add_option(
159 '--find-copies', action='store_true',
160 help='Allows git to look for copies.')
161 parser.add_option(
162 '--no-find-copies', action='store_false', dest='find_copies',
163 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164
165 old_parser_args = parser.parse_args
166 def Parse(args):
167 options, args = old_parser_args(args)
168
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000171 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 print('Note: Saving similarity of %d%% in git config.'
173 % options.similarity)
174 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000175
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 options.similarity = max(0, min(options.similarity, 100))
177
178 if options.find_copies is None:
179 options.find_copies = bool(
180 git_get_branch_default('git-find-copies', True))
181 else:
182 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183
184 print('Using %d%% similarity for rename/copy detection. '
185 'Override with --similarity.' % options.similarity)
186
187 return options, args
188 parser.parse_args = Parse
189
190
ukai@chromium.org259e4682012-10-25 07:36:33 +0000191def is_dirty_git_tree(cmd):
192 # Make sure index is up-to-date before running diff-index.
193 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
194 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
195 if dirty:
196 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
197 print 'Uncommitted files: (git diff-index --name-status HEAD)'
198 print dirty[:4096]
199 if len(dirty) > 4096:
200 print '... (run "git diff-index --name-status HEAD" to see full output).'
201 return True
202 return False
203
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000204
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000205def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
206 """Return the corresponding git ref if |base_url| together with |glob_spec|
207 matches the full |url|.
208
209 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
210 """
211 fetch_suburl, as_ref = glob_spec.split(':')
212 if allow_wildcards:
213 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
214 if glob_match:
215 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
216 # "branches/{472,597,648}/src:refs/remotes/svn/*".
217 branch_re = re.escape(base_url)
218 if glob_match.group(1):
219 branch_re += '/' + re.escape(glob_match.group(1))
220 wildcard = glob_match.group(2)
221 if wildcard == '*':
222 branch_re += '([^/]*)'
223 else:
224 # Escape and replace surrounding braces with parentheses and commas
225 # with pipe symbols.
226 wildcard = re.escape(wildcard)
227 wildcard = re.sub('^\\\\{', '(', wildcard)
228 wildcard = re.sub('\\\\,', '|', wildcard)
229 wildcard = re.sub('\\\\}$', ')', wildcard)
230 branch_re += wildcard
231 if glob_match.group(3):
232 branch_re += re.escape(glob_match.group(3))
233 match = re.match(branch_re, url)
234 if match:
235 return re.sub('\*$', match.group(1), as_ref)
236
237 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
238 if fetch_suburl:
239 full_url = base_url + '/' + fetch_suburl
240 else:
241 full_url = base_url
242 if full_url == url:
243 return as_ref
244 return None
245
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000246
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 """Prints statistics about the change to the user."""
249 # --no-ext-diff is broken in some versions of Git, so try to work around
250 # this by overriding the environment (but there is still a problem if the
251 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000252 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000253 if 'GIT_EXTERNAL_DIFF' in env:
254 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255
256 if find_copies:
257 similarity_options = ['--find-copies-harder', '-l100000',
258 '-C%s' % similarity]
259 else:
260 similarity_options = ['-M%s' % similarity]
261
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000262 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000263 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000264 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000265 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000266
267
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268class Settings(object):
269 def __init__(self):
270 self.default_server = None
271 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000272 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000273 self.is_git_svn = None
274 self.svn_branch = None
275 self.tree_status_url = None
276 self.viewvc_url = None
277 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000278 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000279 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000280
281 def LazyUpdateIfNeeded(self):
282 """Updates the settings from a codereview.settings file, if available."""
283 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000284 # The only value that actually changes the behavior is
285 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000286 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000287 error_ok=True
288 ).strip().lower()
289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000291 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000293 # set updated to True to avoid infinite calling loop
294 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000295 self.updated = True
296 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 self.updated = True
298
299 def GetDefaultServerUrl(self, error_ok=False):
300 if not self.default_server:
301 self.LazyUpdateIfNeeded()
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_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000304 if error_ok:
305 return self.default_server
306 if not self.default_server:
307 error_message = ('Could not find settings file. You must configure '
308 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000309 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000310 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000311 return self.default_server
312
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000313 @staticmethod
314 def GetRelativeRoot():
315 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000317 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000318 if self.root is None:
319 self.root = os.path.abspath(self.GetRelativeRoot())
320 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321
322 def GetIsGitSvn(self):
323 """Return true if this repo looks like it's using git-svn."""
324 if self.is_git_svn is None:
325 # If you have any "svn-remote.*" config keys, we think you're using svn.
326 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000327 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000328 return self.is_git_svn
329
330 def GetSVNBranch(self):
331 if self.svn_branch is None:
332 if not self.GetIsGitSvn():
333 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
334
335 # Try to figure out which remote branch we're based on.
336 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000337 # 1) iterate through our branch history and find the svn URL.
338 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000339
340 # regexp matching the git-svn line that contains the URL.
341 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
342
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000343 # We don't want to go through all of history, so read a line from the
344 # pipe at a time.
345 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000346 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000347 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
348 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000349 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000350 for line in proc.stdout:
351 match = git_svn_re.match(line)
352 if match:
353 url = match.group(1)
354 proc.stdout.close() # Cut pipe.
355 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000356
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000357 if url:
358 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
359 remotes = RunGit(['config', '--get-regexp',
360 r'^svn-remote\..*\.url']).splitlines()
361 for remote in remotes:
362 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000363 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000364 remote = match.group(1)
365 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000366 rewrite_root = RunGit(
367 ['config', 'svn-remote.%s.rewriteRoot' % remote],
368 error_ok=True).strip()
369 if rewrite_root:
370 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000371 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000372 ['config', 'svn-remote.%s.fetch' % remote],
373 error_ok=True).strip()
374 if fetch_spec:
375 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
376 if self.svn_branch:
377 break
378 branch_spec = RunGit(
379 ['config', 'svn-remote.%s.branches' % remote],
380 error_ok=True).strip()
381 if branch_spec:
382 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
383 if self.svn_branch:
384 break
385 tag_spec = RunGit(
386 ['config', 'svn-remote.%s.tags' % remote],
387 error_ok=True).strip()
388 if tag_spec:
389 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
390 if self.svn_branch:
391 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000392
393 if not self.svn_branch:
394 DieWithError('Can\'t guess svn branch -- try specifying it on the '
395 'command line')
396
397 return self.svn_branch
398
399 def GetTreeStatusUrl(self, error_ok=False):
400 if not self.tree_status_url:
401 error_message = ('You must configure your tree status URL by running '
402 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000403 self.tree_status_url = self._GetRietveldConfig(
404 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000405 return self.tree_status_url
406
407 def GetViewVCUrl(self):
408 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000409 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000410 return self.viewvc_url
411
rmistry@google.com90752582014-01-14 21:04:50 +0000412 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000413 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000414
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000415 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000416 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000417
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000418 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000419 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000420
ukai@chromium.orge8077812012-02-03 03:41:46 +0000421 def GetIsGerrit(self):
422 """Return true if this repo is assosiated with gerrit code review system."""
423 if self.is_gerrit is None:
424 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
425 return self.is_gerrit
426
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000427 def GetGitEditor(self):
428 """Return the editor specified in the git config, or None if none is."""
429 if self.git_editor is None:
430 self.git_editor = self._GetConfig('core.editor', error_ok=True)
431 return self.git_editor or None
432
thestig@chromium.org44202a22014-03-11 19:22:18 +0000433 def GetLintRegex(self):
434 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
435 DEFAULT_LINT_REGEX)
436
437 def GetLintIgnoreRegex(self):
438 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
439 DEFAULT_LINT_IGNORE_REGEX)
440
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000441 def _GetRietveldConfig(self, param, **kwargs):
442 return self._GetConfig('rietveld.' + param, **kwargs)
443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000444 def _GetConfig(self, param, **kwargs):
445 self.LazyUpdateIfNeeded()
446 return RunGit(['config', param], **kwargs).strip()
447
448
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449def ShortBranchName(branch):
450 """Convert a name like 'refs/heads/foo' to just 'foo'."""
451 return branch.replace('refs/heads/', '')
452
453
454class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000455 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000457 global settings
458 if not settings:
459 # Happens when git_cl.py is used as a utility library.
460 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000461 settings.GetDefaultServerUrl()
462 self.branchref = branchref
463 if self.branchref:
464 self.branch = ShortBranchName(self.branchref)
465 else:
466 self.branch = None
467 self.rietveld_server = None
468 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000469 self.lookedup_issue = False
470 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 self.has_description = False
472 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000473 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000474 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000475 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000476 self.cc = None
477 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000478 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000479 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000480
481 def GetCCList(self):
482 """Return the users cc'd on this CL.
483
484 Return is a string suitable for passing to gcl with the --cc flag.
485 """
486 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000487 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000488 more_cc = ','.join(self.watchers)
489 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
490 return self.cc
491
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000492 def GetCCListWithoutDefault(self):
493 """Return the users cc'd on this CL excluding default ones."""
494 if self.cc is None:
495 self.cc = ','.join(self.watchers)
496 return self.cc
497
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000498 def SetWatchers(self, watchers):
499 """Set the list of email addresses that should be cc'd based on the changed
500 files in this CL.
501 """
502 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503
504 def GetBranch(self):
505 """Returns the short branch name, e.g. 'master'."""
506 if not self.branch:
507 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
508 self.branch = ShortBranchName(self.branchref)
509 return self.branch
510
511 def GetBranchRef(self):
512 """Returns the full branch name, e.g. 'refs/heads/master'."""
513 self.GetBranch() # Poke the lazy loader.
514 return self.branchref
515
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 @staticmethod
517 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000518 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000519 e.g. 'origin', 'refs/heads/master'
520 """
521 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000522 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
523 error_ok=True).strip()
524 if upstream_branch:
525 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
526 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000527 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
528 error_ok=True).strip()
529 if upstream_branch:
530 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000531 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000532 # Fall back on trying a git-svn upstream branch.
533 if settings.GetIsGitSvn():
534 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000535 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000536 # Else, try to guess the origin remote.
537 remote_branches = RunGit(['branch', '-r']).split()
538 if 'origin/master' in remote_branches:
539 # Fall back on origin/master if it exits.
540 remote = 'origin'
541 upstream_branch = 'refs/heads/master'
542 elif 'origin/trunk' in remote_branches:
543 # Fall back on origin/trunk if it exists. Generally a shared
544 # git-svn clone
545 remote = 'origin'
546 upstream_branch = 'refs/heads/trunk'
547 else:
548 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549Either pass complete "git diff"-style arguments, like
550 git cl upload origin/master
551or verify this branch is set up to track another (via the --track argument to
552"git checkout -b ...").""")
553
554 return remote, upstream_branch
555
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000557 return git_common.get_or_create_merge_base(self.GetBranch(),
558 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000559
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000560 def GetUpstreamBranch(self):
561 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000562 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000563 if remote is not '.':
564 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
565 self.upstream_branch = upstream_branch
566 return self.upstream_branch
567
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000568 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000569 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000570 remote, branch = None, self.GetBranch()
571 seen_branches = set()
572 while branch not in seen_branches:
573 seen_branches.add(branch)
574 remote, branch = self.FetchUpstreamTuple(branch)
575 branch = ShortBranchName(branch)
576 if remote != '.' or branch.startswith('refs/remotes'):
577 break
578 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000579 remotes = RunGit(['remote'], error_ok=True).split()
580 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000581 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000582 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000583 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000584 logging.warning('Could not determine which remote this change is '
585 'associated with, so defaulting to "%s". This may '
586 'not be what you want. You may prevent this message '
587 'by running "git svn info" as documented here: %s',
588 self._remote,
589 GIT_INSTRUCTIONS_URL)
590 else:
591 logging.warn('Could not determine which remote this change is '
592 'associated with. You may prevent this message by '
593 'running "git svn info" as documented here: %s',
594 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000595 branch = 'HEAD'
596 if branch.startswith('refs/remotes'):
597 self._remote = (remote, branch)
598 else:
599 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000600 return self._remote
601
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000602 def GitSanityChecks(self, upstream_git_obj):
603 """Checks git repo status and ensures diff is from local commits."""
604
605 # Verify the commit we're diffing against is in our current branch.
606 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
607 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
608 if upstream_sha != common_ancestor:
609 print >> sys.stderr, (
610 'ERROR: %s is not in the current branch. You may need to rebase '
611 'your tracking branch' % upstream_sha)
612 return False
613
614 # List the commits inside the diff, and verify they are all local.
615 commits_in_diff = RunGit(
616 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
617 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
618 remote_branch = remote_branch.strip()
619 if code != 0:
620 _, remote_branch = self.GetRemoteBranch()
621
622 commits_in_remote = RunGit(
623 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
624
625 common_commits = set(commits_in_diff) & set(commits_in_remote)
626 if common_commits:
627 print >> sys.stderr, (
628 'ERROR: Your diff contains %d commits already in %s.\n'
629 'Run "git log --oneline %s..HEAD" to get a list of commits in '
630 'the diff. If you are using a custom git flow, you can override'
631 ' the reference used for this check with "git config '
632 'gitcl.remotebranch <git-ref>".' % (
633 len(common_commits), remote_branch, upstream_git_obj))
634 return False
635 return True
636
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000637 def GetGitBaseUrlFromConfig(self):
sheyang@google.com6a9ead22014-05-14 20:03:55 +0000638 """Return the configured base URL from branch.<branchname>.canonical-url.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000639
640 Returns None if it is not set.
641 """
sheyang@google.com6a9ead22014-05-14 20:03:55 +0000642 branch = self.GetBranch()
643 url = RunGit(['config', 'branch.%s.canonical-url' % branch],
644 error_ok=True).strip()
645 if not url:
646 url = RunGit(['config', 'branch.%s.base-url' % branch],
647 error_ok=True).strip()
648 return url
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 def GetRemoteUrl(self):
651 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
652
653 Returns None if there is no remote.
654 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000655 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
657
658 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000659 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000660 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000662 self.issue = int(issue) or None if issue else None
663 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000664 return self.issue
665
666 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000667 if not self.rietveld_server:
668 # If we're on a branch then get the server potentially associated
669 # with that branch.
670 if self.GetIssue():
671 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
672 ['config', self._RietveldServer()], error_ok=True).strip())
673 if not self.rietveld_server:
674 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675 return self.rietveld_server
676
677 def GetIssueURL(self):
678 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000679 if not self.GetIssue():
680 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000681 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
682
683 def GetDescription(self, pretty=False):
684 if not self.has_description:
685 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000686 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000687 try:
688 self.description = self.RpcServer().get_description(issue).strip()
689 except urllib2.HTTPError, e:
690 if e.code == 404:
691 DieWithError(
692 ('\nWhile fetching the description for issue %d, received a '
693 '404 (not found)\n'
694 'error. It is likely that you deleted this '
695 'issue on the server. If this is the\n'
696 'case, please run\n\n'
697 ' git cl issue 0\n\n'
698 'to clear the association with the deleted issue. Then run '
699 'this command again.') % issue)
700 else:
701 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000702 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703 self.has_description = True
704 if pretty:
705 wrapper = textwrap.TextWrapper()
706 wrapper.initial_indent = wrapper.subsequent_indent = ' '
707 return wrapper.fill(self.description)
708 return self.description
709
710 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000711 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000712 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713 patchset = RunGit(['config', self._PatchsetSetting()],
714 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000715 self.patchset = int(patchset) or None if patchset else None
716 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 return self.patchset
718
719 def SetPatchset(self, patchset):
720 """Set this branch's patchset. If patchset=0, clears the patchset."""
721 if patchset:
722 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000723 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 else:
725 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000726 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000727 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000728
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000729 def GetMostRecentPatchset(self):
730 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000731
732 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000733 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000734 '/download/issue%s_%s.diff' % (issue, patchset))
735
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000736 def GetIssueProperties(self):
737 if self._props is None:
738 issue = self.GetIssue()
739 if not issue:
740 self._props = {}
741 else:
742 self._props = self.RpcServer().get_issue_properties(issue, True)
743 return self._props
744
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000745 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000746 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000747
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748 def SetIssue(self, issue):
749 """Set this branch's issue. If issue=0, clears the issue."""
750 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000751 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 RunGit(['config', self._IssueSetting(), str(issue)])
753 if self.rietveld_server:
754 RunGit(['config', self._RietveldServer(), self.rietveld_server])
755 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000756 current_issue = self.GetIssue()
757 if current_issue:
758 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000759 self.issue = None
760 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000762 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000763 if not self.GitSanityChecks(upstream_branch):
764 DieWithError('\nGit sanity check failure')
765
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000766 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000767 if not root:
768 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000769 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000770
771 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000772 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000773 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000774 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000775 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000776 except subprocess2.CalledProcessError:
777 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000778 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000779 'This branch probably doesn\'t exist anymore. To reset the\n'
780 'tracking branch, please run\n'
781 ' git branch --set-upstream %s trunk\n'
782 'replacing trunk with origin/master or the relevant branch') %
783 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000784
maruel@chromium.org52424302012-08-29 15:14:30 +0000785 issue = self.GetIssue()
786 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000787 if issue:
788 description = self.GetDescription()
789 else:
790 # If the change was never uploaded, use the log messages of all commits
791 # up to the branch point, as git cl upload will prefill the description
792 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000793 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
794 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000795
796 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000797 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000798 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000799 name,
800 description,
801 absroot,
802 files,
803 issue,
804 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000805 author,
806 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000807
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000808 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000809 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000810
811 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000812 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000813 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000814 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000815 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000816 except presubmit_support.PresubmitFailure, e:
817 DieWithError(
818 ('%s\nMaybe your depot_tools is out of date?\n'
819 'If all fails, contact maruel@') % e)
820
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000821 def UpdateDescription(self, description):
822 self.description = description
823 return self.RpcServer().update_description(
824 self.GetIssue(), self.description)
825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000827 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000828 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000830 def SetFlag(self, flag, value):
831 """Patchset must match."""
832 if not self.GetPatchset():
833 DieWithError('The patchset needs to match. Send another patchset.')
834 try:
835 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000836 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000837 except urllib2.HTTPError, e:
838 if e.code == 404:
839 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
840 if e.code == 403:
841 DieWithError(
842 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
843 'match?') % (self.GetIssue(), self.GetPatchset()))
844 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000846 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847 """Returns an upload.RpcServer() to access this review's rietveld instance.
848 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000849 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000850 self._rpc_server = rietveld.CachingRietveld(
851 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000852 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853
854 def _IssueSetting(self):
855 """Return the git setting that stores this change's issue."""
856 return 'branch.%s.rietveldissue' % self.GetBranch()
857
858 def _PatchsetSetting(self):
859 """Return the git setting that stores this change's most recent patchset."""
860 return 'branch.%s.rietveldpatchset' % self.GetBranch()
861
862 def _RietveldServer(self):
863 """Returns the git setting that stores this change's rietveld server."""
864 return 'branch.%s.rietveldserver' % self.GetBranch()
865
866
867def GetCodereviewSettingsInteractively():
868 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000869 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 server = settings.GetDefaultServerUrl(error_ok=True)
871 prompt = 'Rietveld server (host[:port])'
872 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000873 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874 if not server and not newserver:
875 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000876 if newserver:
877 newserver = gclient_utils.UpgradeToHttps(newserver)
878 if newserver != server:
879 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000881 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 prompt = caption
883 if initial:
884 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000885 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886 if new_val == 'x':
887 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000888 elif new_val:
889 if is_url:
890 new_val = gclient_utils.UpgradeToHttps(new_val)
891 if new_val != initial:
892 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000894 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000895 SetProperty(settings.GetDefaultPrivateFlag(),
896 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000898 'tree-status-url', False)
899 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000900 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000901
902 # TODO: configure a default branch to diff against, rather than this
903 # svn-based hackery.
904
905
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000906class ChangeDescription(object):
907 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000908 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000909 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000910
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000911 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000912 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000913
agable@chromium.org42c20792013-09-12 17:34:49 +0000914 @property # www.logilab.org/ticket/89786
915 def description(self): # pylint: disable=E0202
916 return '\n'.join(self._description_lines)
917
918 def set_description(self, desc):
919 if isinstance(desc, basestring):
920 lines = desc.splitlines()
921 else:
922 lines = [line.rstrip() for line in desc]
923 while lines and not lines[0]:
924 lines.pop(0)
925 while lines and not lines[-1]:
926 lines.pop(-1)
927 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000928
929 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000930 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000931 assert isinstance(reviewers, list), reviewers
932 if not reviewers:
933 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000934 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000935
agable@chromium.org42c20792013-09-12 17:34:49 +0000936 # Get the set of R= and TBR= lines and remove them from the desciption.
937 regexp = re.compile(self.R_LINE)
938 matches = [regexp.match(line) for line in self._description_lines]
939 new_desc = [l for i, l in enumerate(self._description_lines)
940 if not matches[i]]
941 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000942
agable@chromium.org42c20792013-09-12 17:34:49 +0000943 # Construct new unified R= and TBR= lines.
944 r_names = []
945 tbr_names = []
946 for match in matches:
947 if not match:
948 continue
949 people = cleanup_list([match.group(2).strip()])
950 if match.group(1) == 'TBR':
951 tbr_names.extend(people)
952 else:
953 r_names.extend(people)
954 for name in r_names:
955 if name not in reviewers:
956 reviewers.append(name)
957 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
958 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
959
960 # Put the new lines in the description where the old first R= line was.
961 line_loc = next((i for i, match in enumerate(matches) if match), -1)
962 if 0 <= line_loc < len(self._description_lines):
963 if new_tbr_line:
964 self._description_lines.insert(line_loc, new_tbr_line)
965 if new_r_line:
966 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000967 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000968 if new_r_line:
969 self.append_footer(new_r_line)
970 if new_tbr_line:
971 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000972
973 def prompt(self):
974 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000975 self.set_description([
976 '# Enter a description of the change.',
977 '# This will be displayed on the codereview site.',
978 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000979 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000980 '--------------------',
981 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000982
agable@chromium.org42c20792013-09-12 17:34:49 +0000983 regexp = re.compile(self.BUG_LINE)
984 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000985 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000986 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000987 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000988 if not content:
989 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000990 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000991
992 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000993 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
994 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000995 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000996 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000997
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000998 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000999 if self._description_lines:
1000 # Add an empty line if either the last line or the new line isn't a tag.
1001 last_line = self._description_lines[-1]
1002 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1003 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1004 self._description_lines.append('')
1005 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001006
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001007 def get_reviewers(self):
1008 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001009 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1010 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001011 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001012
1013
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001014def get_approving_reviewers(props):
1015 """Retrieves the reviewers that approved a CL from the issue properties with
1016 messages.
1017
1018 Note that the list may contain reviewers that are not committer, thus are not
1019 considered by the CQ.
1020 """
1021 return sorted(
1022 set(
1023 message['sender']
1024 for message in props['messages']
1025 if message['approval'] and message['sender'] in props['reviewers']
1026 )
1027 )
1028
1029
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030def FindCodereviewSettingsFile(filename='codereview.settings'):
1031 """Finds the given file starting in the cwd and going up.
1032
1033 Only looks up to the top of the repository unless an
1034 'inherit-review-settings-ok' file exists in the root of the repository.
1035 """
1036 inherit_ok_file = 'inherit-review-settings-ok'
1037 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001038 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1040 root = '/'
1041 while True:
1042 if filename in os.listdir(cwd):
1043 if os.path.isfile(os.path.join(cwd, filename)):
1044 return open(os.path.join(cwd, filename))
1045 if cwd == root:
1046 break
1047 cwd = os.path.dirname(cwd)
1048
1049
1050def LoadCodereviewSettingsFromFile(fileobj):
1051 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001052 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054 def SetProperty(name, setting, unset_error_ok=False):
1055 fullname = 'rietveld.' + name
1056 if setting in keyvals:
1057 RunGit(['config', fullname, keyvals[setting]])
1058 else:
1059 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1060
1061 SetProperty('server', 'CODE_REVIEW_SERVER')
1062 # Only server setting is required. Other settings can be absent.
1063 # In that case, we ignore errors raised during option deletion attempt.
1064 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001065 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1067 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001068 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001069 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1070 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001072 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001073 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001074
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1076 #should be of the form
1077 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1078 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1079 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1080 keyvals['ORIGIN_URL_CONFIG']])
1081
sheyang@google.com6a9ead22014-05-14 20:03:55 +00001082 if 'CANONICAL_URL' in keyvals:
1083 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1084 branch = ShortBranchName(branchref)
1085 RunGit(['config', 'branch.%s.canonical-url' % branch,
1086 keyvals['CANONICAL_URL']],
1087 error_ok=False)
1088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001090def urlretrieve(source, destination):
1091 """urllib is broken for SSL connections via a proxy therefore we
1092 can't use urllib.urlretrieve()."""
1093 with open(destination, 'w') as f:
1094 f.write(urllib2.urlopen(source).read())
1095
1096
ukai@chromium.org712d6102013-11-27 00:52:58 +00001097def hasSheBang(fname):
1098 """Checks fname is a #! script."""
1099 with open(fname) as f:
1100 return f.read(2).startswith('#!')
1101
1102
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001103def DownloadHooks(force):
1104 """downloads hooks
1105
1106 Args:
1107 force: True to update hooks. False to install hooks if not present.
1108 """
1109 if not settings.GetIsGerrit():
1110 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001111 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001112 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1113 if not os.access(dst, os.X_OK):
1114 if os.path.exists(dst):
1115 if not force:
1116 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001117 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001118 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001119 if not hasSheBang(dst):
1120 DieWithError('Not a script: %s\n'
1121 'You need to download from\n%s\n'
1122 'into .git/hooks/commit-msg and '
1123 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001124 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1125 except Exception:
1126 if os.path.exists(dst):
1127 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001128 DieWithError('\nFailed to download hooks.\n'
1129 'You need to download from\n%s\n'
1130 'into .git/hooks/commit-msg and '
1131 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001132
1133
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001134@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001136 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001138 parser.add_option('--activate-update', action='store_true',
1139 help='activate auto-updating [rietveld] section in '
1140 '.git/config')
1141 parser.add_option('--deactivate-update', action='store_true',
1142 help='deactivate auto-updating [rietveld] section in '
1143 '.git/config')
1144 options, args = parser.parse_args(args)
1145
1146 if options.deactivate_update:
1147 RunGit(['config', 'rietveld.autoupdate', 'false'])
1148 return
1149
1150 if options.activate_update:
1151 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1152 return
1153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 if len(args) == 0:
1155 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001156 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 return 0
1158
1159 url = args[0]
1160 if not url.endswith('codereview.settings'):
1161 url = os.path.join(url, 'codereview.settings')
1162
1163 # Load code review settings and download hooks (if available).
1164 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001165 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 return 0
1167
1168
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001169def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001170 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001171 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1172 branch = ShortBranchName(branchref)
1173 _, args = parser.parse_args(args)
1174 if not args:
1175 print("Current base-url:")
1176 return RunGit(['config', 'branch.%s.base-url' % branch],
1177 error_ok=False).strip()
1178 else:
1179 print("Setting base-url to %s" % args[0])
1180 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1181 error_ok=False).strip()
1182
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001185 """Show status of changelists.
1186
1187 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001188 - Red not sent for review or broken
1189 - Blue waiting for review
1190 - Yellow waiting for you to reply to review
1191 - Green LGTM'ed
1192 - Magenta in the commit queue
1193 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001194
1195 Also see 'git cl comments'.
1196 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197 parser.add_option('--field',
1198 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001199 parser.add_option('-f', '--fast', action='store_true',
1200 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001202 if args:
1203 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001206 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 if options.field.startswith('desc'):
1208 print cl.GetDescription()
1209 elif options.field == 'id':
1210 issueid = cl.GetIssue()
1211 if issueid:
1212 print issueid
1213 elif options.field == 'patch':
1214 patchset = cl.GetPatchset()
1215 if patchset:
1216 print patchset
1217 elif options.field == 'url':
1218 url = cl.GetIssueURL()
1219 if url:
1220 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001221 return 0
1222
1223 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1224 if not branches:
1225 print('No local branch found.')
1226 return 0
1227
1228 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001229 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001230 alignment = max(5, max(len(b) for b in branches))
1231 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 # Adhoc thread pool to request data concurrently.
1233 output = Queue.Queue()
1234
1235 # Silence upload.py otherwise it becomes unweldly.
1236 upload.verbosity = 0
1237
1238 if not options.fast:
1239 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001240 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001241 c = Changelist(branchref=b)
1242 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001243 props = {}
1244 r = None
1245 if i:
1246 try:
1247 props = c.GetIssueProperties()
1248 r = c.GetApprovingReviewers() if i else None
1249 except urllib2.HTTPError:
1250 # The issue probably doesn't exist anymore.
1251 i += ' (broken)'
1252
1253 msgs = props.get('messages') or []
1254
1255 if not i:
1256 color = Fore.WHITE
1257 elif props.get('closed'):
1258 # Issue is closed.
1259 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001260 elif props.get('commit'):
1261 # Issue is in the commit queue.
1262 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001263 elif r:
1264 # Was LGTM'ed.
1265 color = Fore.GREEN
1266 elif not msgs:
1267 # No message was sent.
1268 color = Fore.RED
1269 elif msgs[-1]['sender'] != props.get('owner_email'):
1270 color = Fore.YELLOW
1271 else:
1272 color = Fore.BLUE
1273 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001274
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001275 # Process one branch synchronously to work through authentication, then
1276 # spawn threads to process all the other branches in parallel.
1277 if branches:
1278 fetch(branches[0])
1279 threads = [
1280 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001281 for t in threads:
1282 t.daemon = True
1283 t.start()
1284 else:
1285 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1286 for b in branches:
1287 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001288 url = c.GetIssueURL()
1289 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001290
1291 tmp = {}
1292 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001293 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001294 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001295 b, i, color = output.get()
1296 tmp[b] = (i, color)
1297 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001298 reset = Fore.RESET
1299 if not sys.stdout.isatty():
1300 color = ''
1301 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001302 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001303 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001304
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001305 cl = Changelist()
1306 print
1307 print 'Current branch:',
1308 if not cl.GetIssue():
1309 print 'no issue assigned.'
1310 return 0
1311 print cl.GetBranch()
1312 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1313 print 'Issue description:'
1314 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315 return 0
1316
1317
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001318def colorize_CMDstatus_doc():
1319 """To be called once in main() to add colors to git cl status help."""
1320 colors = [i for i in dir(Fore) if i[0].isupper()]
1321
1322 def colorize_line(line):
1323 for color in colors:
1324 if color in line.upper():
1325 # Extract whitespaces first and the leading '-'.
1326 indent = len(line) - len(line.lstrip(' ')) + 1
1327 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1328 return line
1329
1330 lines = CMDstatus.__doc__.splitlines()
1331 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1332
1333
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001334@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001336 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337
1338 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001339 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001340 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341
1342 cl = Changelist()
1343 if len(args) > 0:
1344 try:
1345 issue = int(args[0])
1346 except ValueError:
1347 DieWithError('Pass a number to set the issue or none to list it.\n'
1348 'Maybe you want to run git cl status?')
1349 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001350 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351 return 0
1352
1353
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001354def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001355 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001356 (_, args) = parser.parse_args(args)
1357 if args:
1358 parser.error('Unsupported argument: %s' % args)
1359
1360 cl = Changelist()
1361 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001362 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001363 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001364 if message['disapproval']:
1365 color = Fore.RED
1366 elif message['approval']:
1367 color = Fore.GREEN
1368 elif message['sender'] == data['owner_email']:
1369 color = Fore.MAGENTA
1370 else:
1371 color = Fore.BLUE
1372 print '\n%s%s %s%s' % (
1373 color, message['date'].split('.', 1)[0], message['sender'],
1374 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001375 if message['text'].strip():
1376 print '\n'.join(' ' + l for l in message['text'].splitlines())
1377 return 0
1378
1379
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001380def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001381 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001382 cl = Changelist()
1383 if not cl.GetIssue():
1384 DieWithError('This branch has no associated changelist.')
1385 description = ChangeDescription(cl.GetDescription())
1386 description.prompt()
1387 cl.UpdateDescription(description.description)
1388 return 0
1389
1390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001391def CreateDescriptionFromLog(args):
1392 """Pulls out the commit log to use as a base for the CL description."""
1393 log_args = []
1394 if len(args) == 1 and not args[0].endswith('.'):
1395 log_args = [args[0] + '..']
1396 elif len(args) == 1 and args[0].endswith('...'):
1397 log_args = [args[0][:-1]]
1398 elif len(args) == 2:
1399 log_args = [args[0] + '..' + args[1]]
1400 else:
1401 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001402 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403
1404
thestig@chromium.org44202a22014-03-11 19:22:18 +00001405def CMDlint(parser, args):
1406 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001407 parser.add_option('--filter', action='append', metavar='-x,+y',
1408 help='Comma-separated list of cpplint\'s category-filters')
1409 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001410
1411 # Access to a protected member _XX of a client class
1412 # pylint: disable=W0212
1413 try:
1414 import cpplint
1415 import cpplint_chromium
1416 except ImportError:
1417 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1418 return 1
1419
1420 # Change the current working directory before calling lint so that it
1421 # shows the correct base.
1422 previous_cwd = os.getcwd()
1423 os.chdir(settings.GetRoot())
1424 try:
1425 cl = Changelist()
1426 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1427 files = [f.LocalPath() for f in change.AffectedFiles()]
1428
1429 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001430 command = args + files
1431 if options.filter:
1432 command = ['--filter=' + ','.join(options.filter)] + command
1433 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001434
1435 white_regex = re.compile(settings.GetLintRegex())
1436 black_regex = re.compile(settings.GetLintIgnoreRegex())
1437 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1438 for filename in filenames:
1439 if white_regex.match(filename):
1440 if black_regex.match(filename):
1441 print "Ignoring file %s" % filename
1442 else:
1443 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1444 extra_check_functions)
1445 else:
1446 print "Skipping file %s" % filename
1447 finally:
1448 os.chdir(previous_cwd)
1449 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1450 if cpplint._cpplint_state.error_count != 0:
1451 return 1
1452 return 0
1453
1454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001456 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001457 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001459 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001460 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 (options, args) = parser.parse_args(args)
1462
ukai@chromium.org259e4682012-10-25 07:36:33 +00001463 if not options.force and is_dirty_git_tree('presubmit'):
1464 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 return 1
1466
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001467 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 if args:
1469 base_branch = args[0]
1470 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001471 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001472 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001474 cl.RunHook(
1475 committing=not options.upload,
1476 may_prompt=False,
1477 verbose=options.verbose,
1478 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001479 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480
1481
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001482def AddChangeIdToCommitMessage(options, args):
1483 """Re-commits using the current message, assumes the commit hook is in
1484 place.
1485 """
1486 log_desc = options.message or CreateDescriptionFromLog(args)
1487 git_command = ['commit', '--amend', '-m', log_desc]
1488 RunGit(git_command)
1489 new_log_desc = CreateDescriptionFromLog(args)
1490 if CHANGE_ID in new_log_desc:
1491 print 'git-cl: Added Change-Id to commit message.'
1492 else:
1493 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1494
1495
ukai@chromium.orge8077812012-02-03 03:41:46 +00001496def GerritUpload(options, args, cl):
1497 """upload the current branch to gerrit."""
1498 # We assume the remote called "origin" is the one we want.
1499 # It is probably not worthwhile to support different workflows.
1500 remote = 'origin'
1501 branch = 'master'
1502 if options.target_branch:
1503 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001505 change_desc = ChangeDescription(
1506 options.message or CreateDescriptionFromLog(args))
1507 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001508 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001509 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001510 if CHANGE_ID not in change_desc.description:
1511 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001512
1513 commits = RunGit(['rev-list', '%s/%s...' % (remote, branch)]).splitlines()
1514 if len(commits) > 1:
1515 print('WARNING: This will upload %d commits. Run the following command '
1516 'to see which commits will be uploaded: ' % len(commits))
1517 print('git log %s/%s...' % (remote, branch))
1518 print('You can also use `git squash-branch` to squash these into a single'
1519 'commit.')
1520 ask_for_data('About to upload; enter to confirm.')
1521
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001522 if options.reviewers:
1523 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001524
ukai@chromium.orge8077812012-02-03 03:41:46 +00001525 receive_options = []
1526 cc = cl.GetCCList().split(',')
1527 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001528 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001529 cc = filter(None, cc)
1530 if cc:
1531 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001532 if change_desc.get_reviewers():
1533 receive_options.extend(
1534 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001535
ukai@chromium.orge8077812012-02-03 03:41:46 +00001536 git_command = ['push']
1537 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001538 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001539 ' '.join(receive_options))
1540 git_command += [remote, 'HEAD:refs/for/' + branch]
1541 RunGit(git_command)
1542 # TODO(ukai): parse Change-Id: and set issue number?
1543 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001544
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545
ukai@chromium.orge8077812012-02-03 03:41:46 +00001546def RietveldUpload(options, args, cl):
1547 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1549 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 if options.emulate_svn_auto_props:
1551 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552
1553 change_desc = None
1554
pgervais@chromium.org91141372014-01-09 23:27:20 +00001555 if options.email is not None:
1556 upload_args.extend(['--email', options.email])
1557
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001558 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001559 if options.title:
1560 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001561 if options.message:
1562 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001563 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564 print ("This branch is associated with issue %s. "
1565 "Adding patch to that issue." % cl.GetIssue())
1566 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001567 if options.title:
1568 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001569 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001570 change_desc = ChangeDescription(message)
1571 if options.reviewers:
1572 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001573 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001574 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001575
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001576 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 print "Description is empty; aborting."
1578 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001579
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001580 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001581 if change_desc.get_reviewers():
1582 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001583 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001584 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001585 DieWithError("Must specify reviewers to send email.")
1586 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001587
1588 # We check this before applying rietveld.private assuming that in
1589 # rietveld.cc only addresses which we can send private CLs to are listed
1590 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1591 # --private is specified explicitly on the command line.
1592 if options.private:
1593 logging.warn('rietveld.cc is ignored since private flag is specified. '
1594 'You need to review and add them manually if necessary.')
1595 cc = cl.GetCCListWithoutDefault()
1596 else:
1597 cc = cl.GetCCList()
1598 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001599 if cc:
1600 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001601
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001602 if options.private or settings.GetDefaultPrivateFlag() == "True":
1603 upload_args.append('--private')
1604
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001605 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001606 if not options.find_copies:
1607 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001608
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 # Include the upstream repo's URL in the change -- this is useful for
1610 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001611 remote_url = cl.GetGitBaseUrlFromConfig()
1612 if not remote_url:
1613 if settings.GetIsGitSvn():
1614 # URL is dependent on the current directory.
1615 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1616 if data:
1617 keys = dict(line.split(': ', 1) for line in data.splitlines()
1618 if ': ' in line)
1619 remote_url = keys.get('URL', None)
1620 else:
1621 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1622 remote_url = (cl.GetRemoteUrl() + '@'
1623 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624 if remote_url:
1625 upload_args.extend(['--base_url', remote_url])
1626
1627 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001628 upload_args = ['upload'] + upload_args + args
1629 logging.info('upload.RealMain(%s)', upload_args)
1630 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001631 issue = int(issue)
1632 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001633 except KeyboardInterrupt:
1634 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001635 except:
1636 # If we got an exception after the user typed a description for their
1637 # change, back up the description before re-raising.
1638 if change_desc:
1639 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1640 print '\nGot exception while uploading -- saving description to %s\n' \
1641 % backup_path
1642 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001643 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001644 backup_file.close()
1645 raise
1646
1647 if not cl.GetIssue():
1648 cl.SetIssue(issue)
1649 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001650
1651 if options.use_commit_queue:
1652 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 return 0
1654
1655
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001656def cleanup_list(l):
1657 """Fixes a list so that comma separated items are put as individual items.
1658
1659 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1660 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1661 """
1662 items = sum((i.split(',') for i in l), [])
1663 stripped_items = (i.strip() for i in items)
1664 return sorted(filter(None, stripped_items))
1665
1666
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001667@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001668def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001669 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001670 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1671 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001672 parser.add_option('--bypass-watchlists', action='store_true',
1673 dest='bypass_watchlists',
1674 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001675 parser.add_option('-f', action='store_true', dest='force',
1676 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001677 parser.add_option('-m', dest='message', help='message for patchset')
1678 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001679 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001680 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001681 help='reviewer email addresses')
1682 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001683 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001684 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001685 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001686 help='send email to reviewer immediately')
1687 parser.add_option("--emulate_svn_auto_props", action="store_true",
1688 dest="emulate_svn_auto_props",
1689 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001690 parser.add_option('-c', '--use-commit-queue', action='store_true',
1691 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001692 parser.add_option('--private', action='store_true',
1693 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001694 parser.add_option('--target_branch',
1695 help='When uploading to gerrit, remote branch to '
1696 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001697 parser.add_option('--email', default=None,
1698 help='email address to use to connect to Rietveld')
1699
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001700 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001701 (options, args) = parser.parse_args(args)
1702
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001703 if options.target_branch and not settings.GetIsGerrit():
1704 parser.error('Use --target_branch for non gerrit repository.')
1705
ukai@chromium.org259e4682012-10-25 07:36:33 +00001706 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001707 return 1
1708
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001709 options.reviewers = cleanup_list(options.reviewers)
1710 options.cc = cleanup_list(options.cc)
1711
ukai@chromium.orge8077812012-02-03 03:41:46 +00001712 cl = Changelist()
1713 if args:
1714 # TODO(ukai): is it ok for gerrit case?
1715 base_branch = args[0]
1716 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001717 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001718 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001719 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001720
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001721 # Apply watchlists on upload.
1722 change = cl.GetChange(base_branch, None)
1723 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1724 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001725 if not options.bypass_watchlists:
1726 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001727
ukai@chromium.orge8077812012-02-03 03:41:46 +00001728 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001729 if options.reviewers:
1730 # Set the reviewer list now so that presubmit checks can access it.
1731 change_description = ChangeDescription(change.FullDescriptionText())
1732 change_description.update_reviewers(options.reviewers)
1733 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001734 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001735 may_prompt=not options.force,
1736 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001737 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001738 if not hook_results.should_continue():
1739 return 1
1740 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001741 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001742
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001743 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001744 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001745 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001746 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001747 print ('The last upload made from this repository was patchset #%d but '
1748 'the most recent patchset on the server is #%d.'
1749 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001750 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1751 'from another machine or branch the patch you\'re uploading now '
1752 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001753 ask_for_data('About to upload; enter to confirm.')
1754
iannucci@chromium.org79540052012-10-19 23:15:26 +00001755 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001756 if settings.GetIsGerrit():
1757 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001758 ret = RietveldUpload(options, args, cl)
1759 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001760 git_set_branch_value('last-upload-hash',
1761 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001762
1763 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001764
1765
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001766def IsSubmoduleMergeCommit(ref):
1767 # When submodules are added to the repo, we expect there to be a single
1768 # non-git-svn merge commit at remote HEAD with a signature comment.
1769 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001770 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001771 return RunGit(cmd) != ''
1772
1773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001774def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001775 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001776
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001777 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001778 Updates changelog with metadata (e.g. pointer to review).
1779 Pushes/dcommits the code upstream.
1780 Updates review and closes.
1781 """
1782 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1783 help='bypass upload presubmit hook')
1784 parser.add_option('-m', dest='message',
1785 help="override review description")
1786 parser.add_option('-f', action='store_true', dest='force',
1787 help="force yes to questions (don't prompt)")
1788 parser.add_option('-c', dest='contributor',
1789 help="external contributor for patch (appended to " +
1790 "description and used as author for git). Should be " +
1791 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001792 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001793 (options, args) = parser.parse_args(args)
1794 cl = Changelist()
1795
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001796 current = cl.GetBranch()
1797 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1798 if not settings.GetIsGitSvn() and remote == '.':
1799 print
1800 print 'Attempting to push branch %r into another local branch!' % current
1801 print
1802 print 'Either reparent this branch on top of origin/master:'
1803 print ' git reparent-branch --root'
1804 print
1805 print 'OR run `git rebase-update` if you think the parent branch is already'
1806 print 'committed.'
1807 print
1808 print ' Current parent: %r' % upstream_branch
1809 return 1
1810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811 if not args or cmd == 'push':
1812 # Default to merging against our best guess of the upstream branch.
1813 args = [cl.GetUpstreamBranch()]
1814
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001815 if options.contributor:
1816 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1817 print "Please provide contibutor as 'First Last <email@example.com>'"
1818 return 1
1819
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001821 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001822
ukai@chromium.org259e4682012-10-25 07:36:33 +00001823 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824 return 1
1825
1826 # This rev-list syntax means "show all commits not in my branch that
1827 # are in base_branch".
1828 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1829 base_branch]).splitlines()
1830 if upstream_commits:
1831 print ('Base branch "%s" has %d commits '
1832 'not in this branch.' % (base_branch, len(upstream_commits)))
1833 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1834 return 1
1835
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001836 # This is the revision `svn dcommit` will commit on top of.
1837 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1838 '--pretty=format:%H'])
1839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001841 # If the base_head is a submodule merge commit, the first parent of the
1842 # base_head should be a git-svn commit, which is what we're interested in.
1843 base_svn_head = base_branch
1844 if base_has_submodules:
1845 base_svn_head += '^1'
1846
1847 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001848 if extra_commits:
1849 print ('This branch has %d additional commits not upstreamed yet.'
1850 % len(extra_commits.splitlines()))
1851 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1852 'before attempting to %s.' % (base_branch, cmd))
1853 return 1
1854
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001855 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001856 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001857 author = None
1858 if options.contributor:
1859 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001860 hook_results = cl.RunHook(
1861 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001862 may_prompt=not options.force,
1863 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001864 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001865 if not hook_results.should_continue():
1866 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867
1868 if cmd == 'dcommit':
1869 # Check the tree status if the tree status URL is set.
1870 status = GetTreeStatus()
1871 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001872 print('The tree is closed. Please wait for it to reopen. Use '
1873 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874 return 1
1875 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001876 print('Unable to determine tree status. Please verify manually and '
1877 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001878 else:
1879 breakpad.SendStack(
1880 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001881 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1882 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001883 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001885 change_desc = ChangeDescription(options.message)
1886 if not change_desc.description and cl.GetIssue():
1887 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001888
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001889 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001890 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001891 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001892 else:
1893 print 'No description set.'
1894 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1895 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001897 # Keep a separate copy for the commit message, because the commit message
1898 # contains the link to the Rietveld issue, while the Rietveld message contains
1899 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001900 # Keep a separate copy for the commit message.
1901 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001902 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001903
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001904 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001905 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001906 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001908 commit_desc.append_footer('Patch from %s.' % options.contributor)
1909
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001910 print('Description:')
1911 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912
1913 branches = [base_branch, cl.GetBranchRef()]
1914 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001915 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001916 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001917
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001918 # We want to squash all this branch's commits into one commit with the proper
1919 # description. We do this by doing a "reset --soft" to the base branch (which
1920 # keeps the working copy the same), then dcommitting that. If origin/master
1921 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1922 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001924 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1925 # Delete the branches if they exist.
1926 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1927 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1928 result = RunGitWithCode(showref_cmd)
1929 if result[0] == 0:
1930 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931
1932 # We might be in a directory that's present in this branch but not in the
1933 # trunk. Move up to the top of the tree so that git commands that expect a
1934 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001935 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936 if rel_base_path:
1937 os.chdir(rel_base_path)
1938
1939 # Stuff our change into the merge branch.
1940 # We wrap in a try...finally block so if anything goes wrong,
1941 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001942 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001944 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1945 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001947 RunGit(
1948 [
1949 'commit', '--author', options.contributor,
1950 '-m', commit_desc.description,
1951 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001953 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001954 if base_has_submodules:
1955 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1956 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1957 RunGit(['checkout', CHERRY_PICK_BRANCH])
1958 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959 if cmd == 'push':
1960 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001961 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 retcode, output = RunGitWithCode(
1963 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1964 logging.debug(output)
1965 else:
1966 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001967 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001968 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001969 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970 finally:
1971 # And then swap back to the original branch and clean up.
1972 RunGit(['checkout', '-q', cl.GetBranch()])
1973 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001974 if base_has_submodules:
1975 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976
1977 if cl.GetIssue():
1978 if cmd == 'dcommit' and 'Committed r' in output:
1979 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1980 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001981 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1982 for l in output.splitlines(False))
1983 match = filter(None, match)
1984 if len(match) != 1:
1985 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1986 output)
1987 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001988 else:
1989 return 1
1990 viewvc_url = settings.GetViewVCUrl()
1991 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001992 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001993 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001994 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995 print ('Closing issue '
1996 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001997 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001999 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002000 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00002001 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002002 if options.bypass_hooks:
2003 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2004 else:
2005 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002006 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002007 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002008
2009 if retcode == 0:
2010 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2011 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002012 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002013
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002014 return 0
2015
2016
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002017@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002019 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002021 message = """This doesn't appear to be an SVN repository.
2022If your project has a git mirror with an upstream SVN master, you probably need
2023to run 'git svn init', see your project's git mirror documentation.
2024If your project has a true writeable upstream repository, you probably want
2025to run 'git cl push' instead.
2026Choose wisely, if you get this wrong, your commit might appear to succeed but
2027will instead be silently ignored."""
2028 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002029 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030 return SendUpstream(parser, args, 'dcommit')
2031
2032
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002033@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002034def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002035 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036 if settings.GetIsGitSvn():
2037 print('This appears to be an SVN repository.')
2038 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002039 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002040 return SendUpstream(parser, args, 'push')
2041
2042
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002043@subcommand.usage('[upstream branch to apply against]')
2044def CMDpush(parser, args):
2045 """Temporary alias for 'land'.
2046 """
2047 print(
2048 "\n=======\n"
2049 "'git cl push' has been renamed to 'git cl land'.\n"
2050 "Currently they are treated as synonyms, but 'git cl push' will stop\n"
2051 "working after 2014/07/01.\n"
2052 "=======\n")
2053 now = datetime.datetime.utcfromtimestamp(time.time())
2054 if now > datetime.datetime(2014, 7, 1):
2055 print('******\nReally, you should not use this command anymore... \n'
2056 'Pausing 10 sec to help you remember :-)')
2057 for n in xrange(10):
2058 time.sleep(1)
2059 print('%s seconds...' % (n+1))
2060 print('******')
2061 return CMDland(parser, args)
2062
2063
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002064@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002066 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002067 parser.add_option('-b', dest='newbranch',
2068 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002069 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002071 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2072 help='Change to the directory DIR immediately, '
2073 'before doing anything else.')
2074 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002075 help='failed patches spew .rej files rather than '
2076 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2078 help="don't commit after patch applies")
2079 (options, args) = parser.parse_args(args)
2080 if len(args) != 1:
2081 parser.print_help()
2082 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002083 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002085 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002086 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002087
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002088 if options.newbranch:
2089 if options.force:
2090 RunGit(['branch', '-D', options.newbranch],
2091 stderr=subprocess2.PIPE, error_ok=True)
2092 RunGit(['checkout', '-b', options.newbranch,
2093 Changelist().GetUpstreamBranch()])
2094
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002095 return PatchIssue(issue_arg, options.reject, options.nocommit,
2096 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002097
2098
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002099def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002100 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002102 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002103 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002104 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002105 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002107 # Assume it's a URL to the patch. Default to https.
2108 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002109 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002110 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 DieWithError('Must pass an issue ID or full URL for '
2112 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002113 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002114 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002115 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 # Switch up to the top-level directory, if necessary, in preparation for
2118 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002119 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 if top:
2121 os.chdir(top)
2122
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 # Git patches have a/ at the beginning of source paths. We strip that out
2124 # with a sed script rather than the -p flag to patch so we can feed either
2125 # Git or svn-style patches into the same apply command.
2126 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002127 try:
2128 patch_data = subprocess2.check_output(
2129 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2130 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002131 DieWithError('Git patch mungling failed.')
2132 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002134 # We use "git apply" to apply the patch instead of "patch" so that we can
2135 # pick up file adds.
2136 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002137 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002138 if directory:
2139 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002140 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002142 elif IsGitVersionAtLeast('1.7.12'):
2143 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002144 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002145 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002146 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002147 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 DieWithError('Failed to apply the patch')
2149
2150 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002151 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2153 cl = Changelist()
2154 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002155 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002156 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002157 else:
2158 print "Patch applied to index."
2159 return 0
2160
2161
2162def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002163 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002164 # Provide a wrapper for git svn rebase to help avoid accidental
2165 # git svn dcommit.
2166 # It's the only command that doesn't use parser at all since we just defer
2167 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002168
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002169 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002170
2171
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002172def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002173 """Fetches the tree status and returns either 'open', 'closed',
2174 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002175 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002176 if url:
2177 status = urllib2.urlopen(url).read().lower()
2178 if status.find('closed') != -1 or status == '0':
2179 return 'closed'
2180 elif status.find('open') != -1 or status == '1':
2181 return 'open'
2182 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183 return 'unset'
2184
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002185
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002186def GetTreeStatusReason():
2187 """Fetches the tree status from a json url and returns the message
2188 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002189 url = settings.GetTreeStatusUrl()
2190 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002191 connection = urllib2.urlopen(json_url)
2192 status = json.loads(connection.read())
2193 connection.close()
2194 return status['message']
2195
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002198 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002199 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200 status = GetTreeStatus()
2201 if 'unset' == status:
2202 print 'You must configure your tree status URL by running "git cl config".'
2203 return 2
2204
2205 print "The tree is %s" % status
2206 print
2207 print GetTreeStatusReason()
2208 if status != 'open':
2209 return 1
2210 return 0
2211
2212
maruel@chromium.org15192402012-09-06 12:38:29 +00002213def CMDtry(parser, args):
2214 """Triggers a try job through Rietveld."""
2215 group = optparse.OptionGroup(parser, "Try job options")
2216 group.add_option(
2217 "-b", "--bot", action="append",
2218 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2219 "times to specify multiple builders. ex: "
2220 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2221 "the try server waterfall for the builders name and the tests "
2222 "available. Can also be used to specify gtest_filter, e.g. "
2223 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2224 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002225 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002226 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002227 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002228 "-r", "--revision",
2229 help="Revision to use for the try job; default: the "
2230 "revision will be determined by the try server; see "
2231 "its waterfall for more info")
2232 group.add_option(
2233 "-c", "--clobber", action="store_true", default=False,
2234 help="Force a clobber before building; e.g. don't do an "
2235 "incremental build")
2236 group.add_option(
2237 "--project",
2238 help="Override which project to use. Projects are defined "
2239 "server-side to define what default bot set to use")
2240 group.add_option(
2241 "-t", "--testfilter", action="append", default=[],
2242 help=("Apply a testfilter to all the selected builders. Unless the "
2243 "builders configurations are similar, use multiple "
2244 "--bot <builder>:<test> arguments."))
2245 group.add_option(
2246 "-n", "--name", help="Try job name; default to current branch name")
2247 parser.add_option_group(group)
2248 options, args = parser.parse_args(args)
2249
2250 if args:
2251 parser.error('Unknown arguments: %s' % args)
2252
2253 cl = Changelist()
2254 if not cl.GetIssue():
2255 parser.error('Need to upload first')
2256
2257 if not options.name:
2258 options.name = cl.GetBranch()
2259
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002260 if options.bot and not options.master:
2261 parser.error('For manually specified bots please also specify the '
2262 'tryserver master, e.g. "-m tryserver.chromium".')
2263
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002264 def GetMasterMap():
2265 # Process --bot and --testfilter.
2266 if not options.bot:
2267 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002268
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002269 # Get try masters from PRESUBMIT.py files.
2270 masters = presubmit_support.DoGetTryMasters(
2271 change,
2272 change.LocalPaths(),
2273 settings.GetRoot(),
2274 None,
2275 None,
2276 options.verbose,
2277 sys.stdout)
2278 if masters:
2279 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002280
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002281 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2282 options.bot = presubmit_support.DoGetTrySlaves(
2283 change,
2284 change.LocalPaths(),
2285 settings.GetRoot(),
2286 None,
2287 None,
2288 options.verbose,
2289 sys.stdout)
2290 if not options.bot:
2291 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002292
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002293 builders_and_tests = {}
2294 # TODO(machenbach): The old style command-line options don't support
2295 # multiple try masters yet.
2296 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2297 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2298
2299 for bot in old_style:
2300 if ':' in bot:
2301 builder, tests = bot.split(':', 1)
2302 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2303 elif ',' in bot:
2304 parser.error('Specify one bot per --bot flag')
2305 else:
2306 builders_and_tests.setdefault(bot, []).append('defaulttests')
2307
2308 for bot, tests in new_style:
2309 builders_and_tests.setdefault(bot, []).extend(tests)
2310
2311 # Return a master map with one master to be backwards compatible. The
2312 # master name defaults to an empty string, which will cause the master
2313 # not to be set on rietveld (deprecated).
2314 return {options.master: builders_and_tests}
2315
2316 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002317
maruel@chromium.org15192402012-09-06 12:38:29 +00002318 if options.testfilter:
2319 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002320 masters = dict((master, dict(
2321 (b, forced_tests) for b, t in slaves.iteritems()
2322 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002323
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002324 for builders in masters.itervalues():
2325 if any('triggered' in b for b in builders):
2326 print >> sys.stderr, (
2327 'ERROR You are trying to send a job to a triggered bot. This type of'
2328 ' bot requires an\ninitial job from a parent (usually a builder). '
2329 'Instead send your job to the parent.\n'
2330 'Bot list: %s' % builders)
2331 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002332
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002333 patchset = cl.GetMostRecentPatchset()
2334 if patchset and patchset != cl.GetPatchset():
2335 print(
2336 '\nWARNING Mismatch between local config and server. Did a previous '
2337 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2338 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002339 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002340 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002341 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002342 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002343 except urllib2.HTTPError, e:
2344 if e.code == 404:
2345 print('404 from rietveld; '
2346 'did you mean to use "git try" instead of "git cl try"?')
2347 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002348 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002349
2350 for (master, builders) in masters.iteritems():
2351 if master:
2352 print 'Master: %s' % master
2353 length = max(len(builder) for builder in builders)
2354 for builder in sorted(builders):
2355 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002356 return 0
2357
2358
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002359@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002360def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002361 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002362 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002363 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002364 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002366 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002367 if args:
2368 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002369 branch = cl.GetBranch()
2370 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002371 cl = Changelist()
2372 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002373
2374 # Clear configured merge-base, if there is one.
2375 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002376 else:
2377 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002378 return 0
2379
2380
thestig@chromium.org00858c82013-12-02 23:08:03 +00002381def CMDweb(parser, args):
2382 """Opens the current CL in the web browser."""
2383 _, args = parser.parse_args(args)
2384 if args:
2385 parser.error('Unrecognized args: %s' % ' '.join(args))
2386
2387 issue_url = Changelist().GetIssueURL()
2388 if not issue_url:
2389 print >> sys.stderr, 'ERROR No issue to open'
2390 return 1
2391
2392 webbrowser.open(issue_url)
2393 return 0
2394
2395
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002396def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002397 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002398 _, args = parser.parse_args(args)
2399 if args:
2400 parser.error('Unrecognized args: %s' % ' '.join(args))
2401 cl = Changelist()
2402 cl.SetFlag('commit', '1')
2403 return 0
2404
2405
groby@chromium.org411034a2013-02-26 15:12:01 +00002406def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002407 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002408 _, args = parser.parse_args(args)
2409 if args:
2410 parser.error('Unrecognized args: %s' % ' '.join(args))
2411 cl = Changelist()
2412 # Ensure there actually is an issue to close.
2413 cl.GetDescription()
2414 cl.CloseIssue()
2415 return 0
2416
2417
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002418def CMDdiff(parser, args):
2419 """shows differences between local tree and last upload."""
2420 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002421 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002422 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002423 if not issue:
2424 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002425 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002426 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002427
2428 # Create a new branch based on the merge-base
2429 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2430 try:
2431 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002432 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002433 if rtn != 0:
2434 return rtn
2435
2436 # Switch back to starting brand and diff against the temporary
2437 # branch containing the latest rietveld patch.
2438 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2439 finally:
2440 RunGit(['checkout', '-q', branch])
2441 RunGit(['branch', '-D', TMP_BRANCH])
2442
2443 return 0
2444
2445
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002446def CMDowners(parser, args):
2447 """interactively find the owners for reviewing"""
2448 parser.add_option(
2449 '--no-color',
2450 action='store_true',
2451 help='Use this option to disable color output')
2452 options, args = parser.parse_args(args)
2453
2454 author = RunGit(['config', 'user.email']).strip() or None
2455
2456 cl = Changelist()
2457
2458 if args:
2459 if len(args) > 1:
2460 parser.error('Unknown args')
2461 base_branch = args[0]
2462 else:
2463 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002464 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002465
2466 change = cl.GetChange(base_branch, None)
2467 return owners_finder.OwnersFinder(
2468 [f.LocalPath() for f in
2469 cl.GetChange(base_branch, None).AffectedFiles()],
2470 change.RepositoryRoot(), author,
2471 fopen=file, os_path=os.path, glob=glob.glob,
2472 disable_color=options.no_color).run()
2473
2474
enne@chromium.org555cfe42014-01-29 18:21:39 +00002475@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002476def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002477 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002478 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002479 parser.add_option('--full', action='store_true',
2480 help='Reformat the full content of all touched files')
2481 parser.add_option('--dry-run', action='store_true',
2482 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002483 parser.add_option('--diff', action='store_true',
2484 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002485 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002486
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002487 # git diff generates paths against the root of the repository. Change
2488 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002489 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002490 if rel_base_path:
2491 os.chdir(rel_base_path)
2492
digit@chromium.org29e47272013-05-17 17:01:46 +00002493 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002494 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002495 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002496 # Only list the names of modified files.
2497 diff_cmd.append('--name-only')
2498 else:
2499 # Only generate context-less patches.
2500 diff_cmd.append('-U0')
2501
2502 # Grab the merge-base commit, i.e. the upstream commit of the current
2503 # branch when it was created or the last time it was rebased. This is
2504 # to cover the case where the user may have called "git fetch origin",
2505 # moving the origin branch to a newer commit, but hasn't rebased yet.
2506 upstream_commit = None
2507 cl = Changelist()
2508 upstream_branch = cl.GetUpstreamBranch()
2509 if upstream_branch:
2510 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2511 upstream_commit = upstream_commit.strip()
2512
2513 if not upstream_commit:
2514 DieWithError('Could not find base commit for this branch. '
2515 'Are you in detached state?')
2516
2517 diff_cmd.append(upstream_commit)
2518
2519 # Handle source file filtering.
2520 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002521 if args:
2522 for arg in args:
2523 if os.path.isdir(arg):
2524 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2525 elif os.path.isfile(arg):
2526 diff_cmd.append(arg)
2527 else:
2528 DieWithError('Argument "%s" is not a file or a directory' % arg)
2529 else:
2530 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002531 diff_output = RunGit(diff_cmd)
2532
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002533 top_dir = os.path.normpath(
2534 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2535
2536 # Locate the clang-format binary in the checkout
2537 try:
2538 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2539 except clang_format.NotFoundError, e:
2540 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002541
digit@chromium.org29e47272013-05-17 17:01:46 +00002542 if opts.full:
2543 # diff_output is a list of files to send to clang-format.
2544 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002545 if not files:
2546 print "Nothing to format."
2547 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002548 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002549 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002550 cmd.append('-i')
2551 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002552 if opts.diff:
2553 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002554 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002555 env = os.environ.copy()
2556 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002557 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002558 try:
2559 script = clang_format.FindClangFormatScriptInChromiumTree(
2560 'clang-format-diff.py')
2561 except clang_format.NotFoundError, e:
2562 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002563
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002564 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002565 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002566 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002567
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002568 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002569 if opts.diff:
2570 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002571 if opts.dry_run and len(stdout) > 0:
2572 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002573
2574 return 0
2575
2576
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002577class OptionParser(optparse.OptionParser):
2578 """Creates the option parse and add --verbose support."""
2579 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002580 optparse.OptionParser.__init__(
2581 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002582 self.add_option(
2583 '-v', '--verbose', action='count', default=0,
2584 help='Use 2 times for more debugging info')
2585
2586 def parse_args(self, args=None, values=None):
2587 options, args = optparse.OptionParser.parse_args(self, args, values)
2588 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2589 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2590 return options, args
2591
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002592
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002593def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002594 if sys.hexversion < 0x02060000:
2595 print >> sys.stderr, (
2596 '\nYour python version %s is unsupported, please upgrade.\n' %
2597 sys.version.split(' ', 1)[0])
2598 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002599
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002600 # Reload settings.
2601 global settings
2602 settings = Settings()
2603
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002604 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002605 dispatcher = subcommand.CommandDispatcher(__name__)
2606 try:
2607 return dispatcher.execute(OptionParser(), argv)
2608 except urllib2.HTTPError, e:
2609 if e.code != 500:
2610 raise
2611 DieWithError(
2612 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2613 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002614
2615
2616if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002617 # These affect sys.stdout so do it outside of main() to simplify mocks in
2618 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002619 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002620 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002621 sys.exit(main(sys.argv[1:]))