blob: 9ac9470e22b655aa9d81fdef9409a032094eaac0 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
thakis@chromium.org3421c992014-11-02 02:20:32 +000011import base64
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
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000021import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import textwrap
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
thakis@chromium.org3421c992014-11-02 02:20:32 +000027import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028
29try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000030 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031except ImportError:
32 pass
33
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000035from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036from third_party import upload
37import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000038import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000039import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000040import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000041import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000042import git_common
piman@chromium.org336f9122014-09-04 02:16:55 +000043import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000044import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000046import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000048import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000049import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050import watchlists
51
maruel@chromium.org0633fb42013-08-16 20:06:14 +000052__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000054DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000055POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000057GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000058CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059
thestig@chromium.org44202a22014-03-11 19:22:18 +000060# Valid extensions for files we want to lint.
61DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
62DEFAULT_LINT_IGNORE_REGEX = r"$^"
63
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000064# Shortcut since it quickly becomes redundant.
65Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000066
maruel@chromium.orgddd59412011-11-30 14:20:38 +000067# Initialized in main()
68settings = None
69
70
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000072 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073 sys.exit(1)
74
75
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000076def GetNoGitPagerEnv():
77 env = os.environ.copy()
78 # 'cat' is a magical git string that disables pagers on all platforms.
79 env['GIT_PAGER'] = 'cat'
80 return env
81
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000082
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000085 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000086 except subprocess2.CalledProcessError as e:
87 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000088 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000090 'Command "%s" failed.\n%s' % (
91 ' '.join(args), error_message or e.stdout or ''))
92 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093
94
95def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000097 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098
99
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000100def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000102 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000103 if suppress_stderr:
104 stderr = subprocess2.VOID
105 else:
106 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000107 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000108 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000109 stdout=subprocess2.PIPE,
110 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000111 return code, out[0]
112 except ValueError:
113 # When the subprocess fails, it returns None. That triggers a ValueError
114 # when trying to unpack the return value into (out, code).
115 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000116
117
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000118def RunGitSilent(args):
119 """Returns stdout, suppresses stderr and ingores the return code."""
120 return RunGitWithCode(args, suppress_stderr=True)[1]
121
122
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000123def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000124 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000125 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000126 return (version.startswith(prefix) and
127 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000128
129
maruel@chromium.org90541732011-04-01 17:54:18 +0000130def ask_for_data(prompt):
131 try:
132 return raw_input(prompt)
133 except KeyboardInterrupt:
134 # Hide the exception.
135 sys.exit(1)
136
137
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138def git_set_branch_value(key, value):
139 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000140 if not branch:
141 return
142
143 cmd = ['config']
144 if isinstance(value, int):
145 cmd.append('--int')
146 git_key = 'branch.%s.%s' % (branch, key)
147 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000148
149
150def git_get_branch_default(key, default):
151 branch = Changelist().GetBranch()
152 if branch:
153 git_key = 'branch.%s.%s' % (branch, key)
154 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
155 try:
156 return int(stdout.strip())
157 except ValueError:
158 pass
159 return default
160
161
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000162def add_git_similarity(parser):
163 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000164 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000165 help='Sets the percentage that a pair of files need to match in order to'
166 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000167 parser.add_option(
168 '--find-copies', action='store_true',
169 help='Allows git to look for copies.')
170 parser.add_option(
171 '--no-find-copies', action='store_false', dest='find_copies',
172 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000173
174 old_parser_args = parser.parse_args
175 def Parse(args):
176 options, args = old_parser_args(args)
177
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000178 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000179 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000180 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000181 print('Note: Saving similarity of %d%% in git config.'
182 % options.similarity)
183 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 options.similarity = max(0, min(options.similarity, 100))
186
187 if options.find_copies is None:
188 options.find_copies = bool(
189 git_get_branch_default('git-find-copies', True))
190 else:
191 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000192
193 print('Using %d%% similarity for rename/copy detection. '
194 'Override with --similarity.' % options.similarity)
195
196 return options, args
197 parser.parse_args = Parse
198
199
ukai@chromium.org259e4682012-10-25 07:36:33 +0000200def is_dirty_git_tree(cmd):
201 # Make sure index is up-to-date before running diff-index.
202 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
203 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
204 if dirty:
205 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
206 print 'Uncommitted files: (git diff-index --name-status HEAD)'
207 print dirty[:4096]
208 if len(dirty) > 4096:
209 print '... (run "git diff-index --name-status HEAD" to see full output).'
210 return True
211 return False
212
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000213
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000214def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
215 """Return the corresponding git ref if |base_url| together with |glob_spec|
216 matches the full |url|.
217
218 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
219 """
220 fetch_suburl, as_ref = glob_spec.split(':')
221 if allow_wildcards:
222 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
223 if glob_match:
224 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
225 # "branches/{472,597,648}/src:refs/remotes/svn/*".
226 branch_re = re.escape(base_url)
227 if glob_match.group(1):
228 branch_re += '/' + re.escape(glob_match.group(1))
229 wildcard = glob_match.group(2)
230 if wildcard == '*':
231 branch_re += '([^/]*)'
232 else:
233 # Escape and replace surrounding braces with parentheses and commas
234 # with pipe symbols.
235 wildcard = re.escape(wildcard)
236 wildcard = re.sub('^\\\\{', '(', wildcard)
237 wildcard = re.sub('\\\\,', '|', wildcard)
238 wildcard = re.sub('\\\\}$', ')', wildcard)
239 branch_re += wildcard
240 if glob_match.group(3):
241 branch_re += re.escape(glob_match.group(3))
242 match = re.match(branch_re, url)
243 if match:
244 return re.sub('\*$', match.group(1), as_ref)
245
246 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
247 if fetch_suburl:
248 full_url = base_url + '/' + fetch_suburl
249 else:
250 full_url = base_url
251 if full_url == url:
252 return as_ref
253 return None
254
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000255
iannucci@chromium.org79540052012-10-19 23:15:26 +0000256def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000257 """Prints statistics about the change to the user."""
258 # --no-ext-diff is broken in some versions of Git, so try to work around
259 # this by overriding the environment (but there is still a problem if the
260 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000261 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000262 if 'GIT_EXTERNAL_DIFF' in env:
263 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000264
265 if find_copies:
266 similarity_options = ['--find-copies-harder', '-l100000',
267 '-C%s' % similarity]
268 else:
269 similarity_options = ['-M%s' % similarity]
270
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000271 try:
272 stdout = sys.stdout.fileno()
273 except AttributeError:
274 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000275 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000276 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000277 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000278 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000279
280
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000281class Settings(object):
282 def __init__(self):
283 self.default_server = None
284 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000285 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286 self.is_git_svn = None
287 self.svn_branch = None
288 self.tree_status_url = None
289 self.viewvc_url = None
290 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000291 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000292 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000293 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000294 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000295 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296
297 def LazyUpdateIfNeeded(self):
298 """Updates the settings from a codereview.settings file, if available."""
299 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000300 # The only value that actually changes the behavior is
301 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000302 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000303 error_ok=True
304 ).strip().lower()
305
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000306 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000307 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000308 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000309 # set updated to True to avoid infinite calling loop
310 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000311 self.updated = True
312 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000313 self.updated = True
314
315 def GetDefaultServerUrl(self, error_ok=False):
316 if not self.default_server:
317 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000318 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000319 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000320 if error_ok:
321 return self.default_server
322 if not self.default_server:
323 error_message = ('Could not find settings file. You must configure '
324 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000325 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000326 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000327 return self.default_server
328
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000329 @staticmethod
330 def GetRelativeRoot():
331 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000334 if self.root is None:
335 self.root = os.path.abspath(self.GetRelativeRoot())
336 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337
338 def GetIsGitSvn(self):
339 """Return true if this repo looks like it's using git-svn."""
340 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000341 if self.GetPendingRefPrefix():
342 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
343 self.is_git_svn = False
344 else:
345 # If you have any "svn-remote.*" config keys, we think you're using svn.
346 self.is_git_svn = RunGitWithCode(
347 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348 return self.is_git_svn
349
350 def GetSVNBranch(self):
351 if self.svn_branch is None:
352 if not self.GetIsGitSvn():
353 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
354
355 # Try to figure out which remote branch we're based on.
356 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000357 # 1) iterate through our branch history and find the svn URL.
358 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000359
360 # regexp matching the git-svn line that contains the URL.
361 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
362
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000363 # We don't want to go through all of history, so read a line from the
364 # pipe at a time.
365 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000366 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000367 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
368 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000369 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000370 for line in proc.stdout:
371 match = git_svn_re.match(line)
372 if match:
373 url = match.group(1)
374 proc.stdout.close() # Cut pipe.
375 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000376
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000377 if url:
378 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
379 remotes = RunGit(['config', '--get-regexp',
380 r'^svn-remote\..*\.url']).splitlines()
381 for remote in remotes:
382 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000383 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000384 remote = match.group(1)
385 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000386 rewrite_root = RunGit(
387 ['config', 'svn-remote.%s.rewriteRoot' % remote],
388 error_ok=True).strip()
389 if rewrite_root:
390 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000391 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000392 ['config', 'svn-remote.%s.fetch' % remote],
393 error_ok=True).strip()
394 if fetch_spec:
395 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
396 if self.svn_branch:
397 break
398 branch_spec = RunGit(
399 ['config', 'svn-remote.%s.branches' % remote],
400 error_ok=True).strip()
401 if branch_spec:
402 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
403 if self.svn_branch:
404 break
405 tag_spec = RunGit(
406 ['config', 'svn-remote.%s.tags' % remote],
407 error_ok=True).strip()
408 if tag_spec:
409 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
410 if self.svn_branch:
411 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000412
413 if not self.svn_branch:
414 DieWithError('Can\'t guess svn branch -- try specifying it on the '
415 'command line')
416
417 return self.svn_branch
418
419 def GetTreeStatusUrl(self, error_ok=False):
420 if not self.tree_status_url:
421 error_message = ('You must configure your tree status URL by running '
422 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000423 self.tree_status_url = self._GetRietveldConfig(
424 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425 return self.tree_status_url
426
427 def GetViewVCUrl(self):
428 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000429 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 return self.viewvc_url
431
rmistry@google.com90752582014-01-14 21:04:50 +0000432 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000433 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000434
rmistry@google.com5626a922015-02-26 14:03:30 +0000435 def GetRunPostUploadHook(self):
436 run_post_upload_hook = self._GetRietveldConfig(
437 'run-post-upload-hook', error_ok=True)
438 return run_post_upload_hook == "True"
439
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000440 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000441 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000442
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000443 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000444 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000445
ukai@chromium.orge8077812012-02-03 03:41:46 +0000446 def GetIsGerrit(self):
447 """Return true if this repo is assosiated with gerrit code review system."""
448 if self.is_gerrit is None:
449 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
450 return self.is_gerrit
451
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000452 def GetGitEditor(self):
453 """Return the editor specified in the git config, or None if none is."""
454 if self.git_editor is None:
455 self.git_editor = self._GetConfig('core.editor', error_ok=True)
456 return self.git_editor or None
457
thestig@chromium.org44202a22014-03-11 19:22:18 +0000458 def GetLintRegex(self):
459 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
460 DEFAULT_LINT_REGEX)
461
462 def GetLintIgnoreRegex(self):
463 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
464 DEFAULT_LINT_IGNORE_REGEX)
465
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000466 def GetProject(self):
467 if not self.project:
468 self.project = self._GetRietveldConfig('project', error_ok=True)
469 return self.project
470
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000471 def GetForceHttpsCommitUrl(self):
472 if not self.force_https_commit_url:
473 self.force_https_commit_url = self._GetRietveldConfig(
474 'force-https-commit-url', error_ok=True)
475 return self.force_https_commit_url
476
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000477 def GetPendingRefPrefix(self):
478 if not self.pending_ref_prefix:
479 self.pending_ref_prefix = self._GetRietveldConfig(
480 'pending-ref-prefix', error_ok=True)
481 return self.pending_ref_prefix
482
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000483 def _GetRietveldConfig(self, param, **kwargs):
484 return self._GetConfig('rietveld.' + param, **kwargs)
485
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000486 def _GetConfig(self, param, **kwargs):
487 self.LazyUpdateIfNeeded()
488 return RunGit(['config', param], **kwargs).strip()
489
490
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491def ShortBranchName(branch):
492 """Convert a name like 'refs/heads/foo' to just 'foo'."""
493 return branch.replace('refs/heads/', '')
494
495
496class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000497 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000498 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000499 global settings
500 if not settings:
501 # Happens when git_cl.py is used as a utility library.
502 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503 settings.GetDefaultServerUrl()
504 self.branchref = branchref
505 if self.branchref:
506 self.branch = ShortBranchName(self.branchref)
507 else:
508 self.branch = None
509 self.rietveld_server = None
510 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000511 self.lookedup_issue = False
512 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000513 self.has_description = False
514 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000515 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000516 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000517 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000518 self.cc = None
519 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000520 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000521 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000522
523 def GetCCList(self):
524 """Return the users cc'd on this CL.
525
526 Return is a string suitable for passing to gcl with the --cc flag.
527 """
528 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000529 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000530 more_cc = ','.join(self.watchers)
531 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
532 return self.cc
533
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000534 def GetCCListWithoutDefault(self):
535 """Return the users cc'd on this CL excluding default ones."""
536 if self.cc is None:
537 self.cc = ','.join(self.watchers)
538 return self.cc
539
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000540 def SetWatchers(self, watchers):
541 """Set the list of email addresses that should be cc'd based on the changed
542 files in this CL.
543 """
544 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000545
546 def GetBranch(self):
547 """Returns the short branch name, e.g. 'master'."""
548 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000549 branchref = RunGit(['symbolic-ref', 'HEAD'],
550 stderr=subprocess2.VOID, error_ok=True).strip()
551 if not branchref:
552 return None
553 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000554 self.branch = ShortBranchName(self.branchref)
555 return self.branch
556
557 def GetBranchRef(self):
558 """Returns the full branch name, e.g. 'refs/heads/master'."""
559 self.GetBranch() # Poke the lazy loader.
560 return self.branchref
561
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000562 @staticmethod
563 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000564 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000565 e.g. 'origin', 'refs/heads/master'
566 """
567 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000568 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
569 error_ok=True).strip()
570 if upstream_branch:
571 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
572 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000573 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
574 error_ok=True).strip()
575 if upstream_branch:
576 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000577 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000578 # Fall back on trying a git-svn upstream branch.
579 if settings.GetIsGitSvn():
580 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000581 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000582 # Else, try to guess the origin remote.
583 remote_branches = RunGit(['branch', '-r']).split()
584 if 'origin/master' in remote_branches:
585 # Fall back on origin/master if it exits.
586 remote = 'origin'
587 upstream_branch = 'refs/heads/master'
588 elif 'origin/trunk' in remote_branches:
589 # Fall back on origin/trunk if it exists. Generally a shared
590 # git-svn clone
591 remote = 'origin'
592 upstream_branch = 'refs/heads/trunk'
593 else:
594 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000595Either pass complete "git diff"-style arguments, like
596 git cl upload origin/master
597or verify this branch is set up to track another (via the --track argument to
598"git checkout -b ...").""")
599
600 return remote, upstream_branch
601
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000602 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000603 return git_common.get_or_create_merge_base(self.GetBranch(),
604 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 def GetUpstreamBranch(self):
607 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000608 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000609 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000610 upstream_branch = upstream_branch.replace('refs/heads/',
611 'refs/remotes/%s/' % remote)
612 upstream_branch = upstream_branch.replace('refs/branch-heads/',
613 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 self.upstream_branch = upstream_branch
615 return self.upstream_branch
616
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000617 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000618 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000619 remote, branch = None, self.GetBranch()
620 seen_branches = set()
621 while branch not in seen_branches:
622 seen_branches.add(branch)
623 remote, branch = self.FetchUpstreamTuple(branch)
624 branch = ShortBranchName(branch)
625 if remote != '.' or branch.startswith('refs/remotes'):
626 break
627 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000628 remotes = RunGit(['remote'], error_ok=True).split()
629 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000630 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000631 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000632 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000633 logging.warning('Could not determine which remote this change is '
634 'associated with, so defaulting to "%s". This may '
635 'not be what you want. You may prevent this message '
636 'by running "git svn info" as documented here: %s',
637 self._remote,
638 GIT_INSTRUCTIONS_URL)
639 else:
640 logging.warn('Could not determine which remote this change is '
641 'associated with. You may prevent this message by '
642 'running "git svn info" as documented here: %s',
643 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000644 branch = 'HEAD'
645 if branch.startswith('refs/remotes'):
646 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000647 elif branch.startswith('refs/branch-heads/'):
648 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000649 else:
650 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000651 return self._remote
652
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000653 def GitSanityChecks(self, upstream_git_obj):
654 """Checks git repo status and ensures diff is from local commits."""
655
sbc@chromium.org79706062015-01-14 21:18:12 +0000656 if upstream_git_obj is None:
657 if self.GetBranch() is None:
658 print >> sys.stderr, (
659 'ERROR: unable to dertermine current branch (detached HEAD?)')
660 else:
661 print >> sys.stderr, (
662 'ERROR: no upstream branch')
663 return False
664
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000665 # Verify the commit we're diffing against is in our current branch.
666 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
667 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
668 if upstream_sha != common_ancestor:
669 print >> sys.stderr, (
670 'ERROR: %s is not in the current branch. You may need to rebase '
671 'your tracking branch' % upstream_sha)
672 return False
673
674 # List the commits inside the diff, and verify they are all local.
675 commits_in_diff = RunGit(
676 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
677 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
678 remote_branch = remote_branch.strip()
679 if code != 0:
680 _, remote_branch = self.GetRemoteBranch()
681
682 commits_in_remote = RunGit(
683 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
684
685 common_commits = set(commits_in_diff) & set(commits_in_remote)
686 if common_commits:
687 print >> sys.stderr, (
688 'ERROR: Your diff contains %d commits already in %s.\n'
689 'Run "git log --oneline %s..HEAD" to get a list of commits in '
690 'the diff. If you are using a custom git flow, you can override'
691 ' the reference used for this check with "git config '
692 'gitcl.remotebranch <git-ref>".' % (
693 len(common_commits), remote_branch, upstream_git_obj))
694 return False
695 return True
696
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000697 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000698 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000699
700 Returns None if it is not set.
701 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000702 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
703 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000704
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000705 def GetGitSvnRemoteUrl(self):
706 """Return the configured git-svn remote URL parsed from git svn info.
707
708 Returns None if it is not set.
709 """
710 # URL is dependent on the current directory.
711 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
712 if data:
713 keys = dict(line.split(': ', 1) for line in data.splitlines()
714 if ': ' in line)
715 return keys.get('URL', None)
716 return None
717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 def GetRemoteUrl(self):
719 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
720
721 Returns None if there is no remote.
722 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000723 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000724 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
725
726 # If URL is pointing to a local directory, it is probably a git cache.
727 if os.path.isdir(url):
728 url = RunGit(['config', 'remote.%s.url' % remote],
729 error_ok=True,
730 cwd=url).strip()
731 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732
733 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000734 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000735 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000737 self.issue = int(issue) or None if issue else None
738 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.issue
740
741 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000742 if not self.rietveld_server:
743 # If we're on a branch then get the server potentially associated
744 # with that branch.
745 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000746 rietveld_server_config = self._RietveldServer()
747 if rietveld_server_config:
748 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
749 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000750 if not self.rietveld_server:
751 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 return self.rietveld_server
753
754 def GetIssueURL(self):
755 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000756 if not self.GetIssue():
757 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
759
760 def GetDescription(self, pretty=False):
761 if not self.has_description:
762 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000763 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000764 try:
765 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000766 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000767 if e.code == 404:
768 DieWithError(
769 ('\nWhile fetching the description for issue %d, received a '
770 '404 (not found)\n'
771 'error. It is likely that you deleted this '
772 'issue on the server. If this is the\n'
773 'case, please run\n\n'
774 ' git cl issue 0\n\n'
775 'to clear the association with the deleted issue. Then run '
776 'this command again.') % issue)
777 else:
778 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000779 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000780 except urllib2.URLError as e:
781 print >> sys.stderr, (
782 'Warning: Failed to retrieve CL description due to network '
783 'failure.')
784 self.description = ''
785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 self.has_description = True
787 if pretty:
788 wrapper = textwrap.TextWrapper()
789 wrapper.initial_indent = wrapper.subsequent_indent = ' '
790 return wrapper.fill(self.description)
791 return self.description
792
793 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000794 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000795 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 patchset = RunGit(['config', self._PatchsetSetting()],
797 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000798 self.patchset = int(patchset) or None if patchset else None
799 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 return self.patchset
801
802 def SetPatchset(self, patchset):
803 """Set this branch's patchset. If patchset=0, clears the patchset."""
804 if patchset:
805 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000806 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 else:
808 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000809 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000810 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000812 def GetMostRecentPatchset(self):
813 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000814
815 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000816 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000817 '/download/issue%s_%s.diff' % (issue, patchset))
818
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000819 def GetIssueProperties(self):
820 if self._props is None:
821 issue = self.GetIssue()
822 if not issue:
823 self._props = {}
824 else:
825 self._props = self.RpcServer().get_issue_properties(issue, True)
826 return self._props
827
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000828 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000829 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000830
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000831 def AddComment(self, message):
832 return self.RpcServer().add_comment(self.GetIssue(), message)
833
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 def SetIssue(self, issue):
835 """Set this branch's issue. If issue=0, clears the issue."""
836 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000837 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 RunGit(['config', self._IssueSetting(), str(issue)])
839 if self.rietveld_server:
840 RunGit(['config', self._RietveldServer(), self.rietveld_server])
841 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000842 current_issue = self.GetIssue()
843 if current_issue:
844 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000845 self.issue = None
846 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000848 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000849 if not self.GitSanityChecks(upstream_branch):
850 DieWithError('\nGit sanity check failure')
851
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000852 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000853 if not root:
854 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000855 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000856
857 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000858 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000859 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000860 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000861 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000862 except subprocess2.CalledProcessError:
863 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000864 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000865 'This branch probably doesn\'t exist anymore. To reset the\n'
866 'tracking branch, please run\n'
867 ' git branch --set-upstream %s trunk\n'
868 'replacing trunk with origin/master or the relevant branch') %
869 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000870
maruel@chromium.org52424302012-08-29 15:14:30 +0000871 issue = self.GetIssue()
872 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000873 if issue:
874 description = self.GetDescription()
875 else:
876 # If the change was never uploaded, use the log messages of all commits
877 # up to the branch point, as git cl upload will prefill the description
878 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000879 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
880 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000881
882 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000883 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000884 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000885 name,
886 description,
887 absroot,
888 files,
889 issue,
890 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000891 author,
892 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000893
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000894 def GetStatus(self):
895 """Apply a rough heuristic to give a simple summary of an issue's review
896 or CQ status, assuming adherence to a common workflow.
897
898 Returns None if no issue for this branch, or one of the following keywords:
899 * 'error' - error from review tool (including deleted issues)
900 * 'unsent' - not sent for review
901 * 'waiting' - waiting for review
902 * 'reply' - waiting for owner to reply to review
903 * 'lgtm' - LGTM from at least one approved reviewer
904 * 'commit' - in the commit queue
905 * 'closed' - closed
906 """
907 if not self.GetIssue():
908 return None
909
910 try:
911 props = self.GetIssueProperties()
912 except urllib2.HTTPError:
913 return 'error'
914
915 if props.get('closed'):
916 # Issue is closed.
917 return 'closed'
918 if props.get('commit'):
919 # Issue is in the commit queue.
920 return 'commit'
921
922 try:
923 reviewers = self.GetApprovingReviewers()
924 except urllib2.HTTPError:
925 return 'error'
926
927 if reviewers:
928 # Was LGTM'ed.
929 return 'lgtm'
930
931 messages = props.get('messages') or []
932
933 if not messages:
934 # No message was sent.
935 return 'unsent'
936 if messages[-1]['sender'] != props.get('owner_email'):
937 # Non-LGTM reply from non-owner
938 return 'reply'
939 return 'waiting'
940
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000941 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000942 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000943
944 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000945 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000946 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000947 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000948 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000949 except presubmit_support.PresubmitFailure, e:
950 DieWithError(
951 ('%s\nMaybe your depot_tools is out of date?\n'
952 'If all fails, contact maruel@') % e)
953
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000954 def UpdateDescription(self, description):
955 self.description = description
956 return self.RpcServer().update_description(
957 self.GetIssue(), self.description)
958
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000960 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000961 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000963 def SetFlag(self, flag, value):
964 """Patchset must match."""
965 if not self.GetPatchset():
966 DieWithError('The patchset needs to match. Send another patchset.')
967 try:
968 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000969 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000970 except urllib2.HTTPError, e:
971 if e.code == 404:
972 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
973 if e.code == 403:
974 DieWithError(
975 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
976 'match?') % (self.GetIssue(), self.GetPatchset()))
977 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000979 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980 """Returns an upload.RpcServer() to access this review's rietveld instance.
981 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000982 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000983 self._rpc_server = rietveld.CachingRietveld(
984 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000985 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986
987 def _IssueSetting(self):
988 """Return the git setting that stores this change's issue."""
989 return 'branch.%s.rietveldissue' % self.GetBranch()
990
991 def _PatchsetSetting(self):
992 """Return the git setting that stores this change's most recent patchset."""
993 return 'branch.%s.rietveldpatchset' % self.GetBranch()
994
995 def _RietveldServer(self):
996 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000997 branch = self.GetBranch()
998 if branch:
999 return 'branch.%s.rietveldserver' % branch
1000 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001
1002
1003def GetCodereviewSettingsInteractively():
1004 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001005 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006 server = settings.GetDefaultServerUrl(error_ok=True)
1007 prompt = 'Rietveld server (host[:port])'
1008 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001009 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 if not server and not newserver:
1011 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001012 if newserver:
1013 newserver = gclient_utils.UpgradeToHttps(newserver)
1014 if newserver != server:
1015 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001017 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 prompt = caption
1019 if initial:
1020 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001021 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022 if new_val == 'x':
1023 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001024 elif new_val:
1025 if is_url:
1026 new_val = gclient_utils.UpgradeToHttps(new_val)
1027 if new_val != initial:
1028 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001030 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001031 SetProperty(settings.GetDefaultPrivateFlag(),
1032 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001034 'tree-status-url', False)
1035 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001036 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001037 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1038 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039
1040 # TODO: configure a default branch to diff against, rather than this
1041 # svn-based hackery.
1042
1043
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001044class ChangeDescription(object):
1045 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001046 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001047 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001048
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001049 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001050 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001051
agable@chromium.org42c20792013-09-12 17:34:49 +00001052 @property # www.logilab.org/ticket/89786
1053 def description(self): # pylint: disable=E0202
1054 return '\n'.join(self._description_lines)
1055
1056 def set_description(self, desc):
1057 if isinstance(desc, basestring):
1058 lines = desc.splitlines()
1059 else:
1060 lines = [line.rstrip() for line in desc]
1061 while lines and not lines[0]:
1062 lines.pop(0)
1063 while lines and not lines[-1]:
1064 lines.pop(-1)
1065 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001066
piman@chromium.org336f9122014-09-04 02:16:55 +00001067 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001068 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001069 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001070 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001071 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001072 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001073
agable@chromium.org42c20792013-09-12 17:34:49 +00001074 # Get the set of R= and TBR= lines and remove them from the desciption.
1075 regexp = re.compile(self.R_LINE)
1076 matches = [regexp.match(line) for line in self._description_lines]
1077 new_desc = [l for i, l in enumerate(self._description_lines)
1078 if not matches[i]]
1079 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001080
agable@chromium.org42c20792013-09-12 17:34:49 +00001081 # Construct new unified R= and TBR= lines.
1082 r_names = []
1083 tbr_names = []
1084 for match in matches:
1085 if not match:
1086 continue
1087 people = cleanup_list([match.group(2).strip()])
1088 if match.group(1) == 'TBR':
1089 tbr_names.extend(people)
1090 else:
1091 r_names.extend(people)
1092 for name in r_names:
1093 if name not in reviewers:
1094 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001095 if add_owners_tbr:
1096 owners_db = owners.Database(change.RepositoryRoot(),
1097 fopen=file, os_path=os.path, glob=glob.glob)
1098 all_reviewers = set(tbr_names + reviewers)
1099 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1100 all_reviewers)
1101 tbr_names.extend(owners_db.reviewers_for(missing_files,
1102 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001103 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1104 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1105
1106 # Put the new lines in the description where the old first R= line was.
1107 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1108 if 0 <= line_loc < len(self._description_lines):
1109 if new_tbr_line:
1110 self._description_lines.insert(line_loc, new_tbr_line)
1111 if new_r_line:
1112 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001113 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001114 if new_r_line:
1115 self.append_footer(new_r_line)
1116 if new_tbr_line:
1117 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001118
1119 def prompt(self):
1120 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001121 self.set_description([
1122 '# Enter a description of the change.',
1123 '# This will be displayed on the codereview site.',
1124 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001125 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001126 '--------------------',
1127 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001128
agable@chromium.org42c20792013-09-12 17:34:49 +00001129 regexp = re.compile(self.BUG_LINE)
1130 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001131 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001132 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001133 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001134 if not content:
1135 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001136 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001137
1138 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001139 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1140 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001141 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001142 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001143
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001144 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001145 if self._description_lines:
1146 # Add an empty line if either the last line or the new line isn't a tag.
1147 last_line = self._description_lines[-1]
1148 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1149 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1150 self._description_lines.append('')
1151 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001152
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001153 def get_reviewers(self):
1154 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001155 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1156 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001157 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001158
1159
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001160def get_approving_reviewers(props):
1161 """Retrieves the reviewers that approved a CL from the issue properties with
1162 messages.
1163
1164 Note that the list may contain reviewers that are not committer, thus are not
1165 considered by the CQ.
1166 """
1167 return sorted(
1168 set(
1169 message['sender']
1170 for message in props['messages']
1171 if message['approval'] and message['sender'] in props['reviewers']
1172 )
1173 )
1174
1175
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176def FindCodereviewSettingsFile(filename='codereview.settings'):
1177 """Finds the given file starting in the cwd and going up.
1178
1179 Only looks up to the top of the repository unless an
1180 'inherit-review-settings-ok' file exists in the root of the repository.
1181 """
1182 inherit_ok_file = 'inherit-review-settings-ok'
1183 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001184 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1186 root = '/'
1187 while True:
1188 if filename in os.listdir(cwd):
1189 if os.path.isfile(os.path.join(cwd, filename)):
1190 return open(os.path.join(cwd, filename))
1191 if cwd == root:
1192 break
1193 cwd = os.path.dirname(cwd)
1194
1195
1196def LoadCodereviewSettingsFromFile(fileobj):
1197 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001198 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200 def SetProperty(name, setting, unset_error_ok=False):
1201 fullname = 'rietveld.' + name
1202 if setting in keyvals:
1203 RunGit(['config', fullname, keyvals[setting]])
1204 else:
1205 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1206
1207 SetProperty('server', 'CODE_REVIEW_SERVER')
1208 # Only server setting is required. Other settings can be absent.
1209 # In that case, we ignore errors raised during option deletion attempt.
1210 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001211 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1213 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001214 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001215 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001216 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1217 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001218 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001219 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001220 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001221 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1222 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001224 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001225 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1228 #should be of the form
1229 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1230 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1231 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1232 keyvals['ORIGIN_URL_CONFIG']])
1233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001235def urlretrieve(source, destination):
1236 """urllib is broken for SSL connections via a proxy therefore we
1237 can't use urllib.urlretrieve()."""
1238 with open(destination, 'w') as f:
1239 f.write(urllib2.urlopen(source).read())
1240
1241
ukai@chromium.org712d6102013-11-27 00:52:58 +00001242def hasSheBang(fname):
1243 """Checks fname is a #! script."""
1244 with open(fname) as f:
1245 return f.read(2).startswith('#!')
1246
1247
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001248def DownloadHooks(force):
1249 """downloads hooks
1250
1251 Args:
1252 force: True to update hooks. False to install hooks if not present.
1253 """
1254 if not settings.GetIsGerrit():
1255 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001256 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001257 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1258 if not os.access(dst, os.X_OK):
1259 if os.path.exists(dst):
1260 if not force:
1261 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001262 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001263 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001264 if not hasSheBang(dst):
1265 DieWithError('Not a script: %s\n'
1266 'You need to download from\n%s\n'
1267 'into .git/hooks/commit-msg and '
1268 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001269 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1270 except Exception:
1271 if os.path.exists(dst):
1272 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001273 DieWithError('\nFailed to download hooks.\n'
1274 'You need to download from\n%s\n'
1275 'into .git/hooks/commit-msg and '
1276 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001277
1278
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001279@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001281 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001283 parser.add_option('--activate-update', action='store_true',
1284 help='activate auto-updating [rietveld] section in '
1285 '.git/config')
1286 parser.add_option('--deactivate-update', action='store_true',
1287 help='deactivate auto-updating [rietveld] section in '
1288 '.git/config')
1289 options, args = parser.parse_args(args)
1290
1291 if options.deactivate_update:
1292 RunGit(['config', 'rietveld.autoupdate', 'false'])
1293 return
1294
1295 if options.activate_update:
1296 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1297 return
1298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 if len(args) == 0:
1300 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001301 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 return 0
1303
1304 url = args[0]
1305 if not url.endswith('codereview.settings'):
1306 url = os.path.join(url, 'codereview.settings')
1307
1308 # Load code review settings and download hooks (if available).
1309 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001310 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 return 0
1312
1313
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001314def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001315 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001316 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1317 branch = ShortBranchName(branchref)
1318 _, args = parser.parse_args(args)
1319 if not args:
1320 print("Current base-url:")
1321 return RunGit(['config', 'branch.%s.base-url' % branch],
1322 error_ok=False).strip()
1323 else:
1324 print("Setting base-url to %s" % args[0])
1325 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1326 error_ok=False).strip()
1327
1328
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001329def color_for_status(status):
1330 """Maps a Changelist status to color, for CMDstatus and other tools."""
1331 return {
1332 'unsent': Fore.RED,
1333 'waiting': Fore.BLUE,
1334 'reply': Fore.YELLOW,
1335 'lgtm': Fore.GREEN,
1336 'commit': Fore.MAGENTA,
1337 'closed': Fore.CYAN,
1338 'error': Fore.WHITE,
1339 }.get(status, Fore.WHITE)
1340
1341
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001343 """Show status of changelists.
1344
1345 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001346 - Red not sent for review or broken
1347 - Blue waiting for review
1348 - Yellow waiting for you to reply to review
1349 - Green LGTM'ed
1350 - Magenta in the commit queue
1351 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001352
1353 Also see 'git cl comments'.
1354 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 parser.add_option('--field',
1356 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001357 parser.add_option('-f', '--fast', action='store_true',
1358 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001360 if args:
1361 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001364 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 if options.field.startswith('desc'):
1366 print cl.GetDescription()
1367 elif options.field == 'id':
1368 issueid = cl.GetIssue()
1369 if issueid:
1370 print issueid
1371 elif options.field == 'patch':
1372 patchset = cl.GetPatchset()
1373 if patchset:
1374 print patchset
1375 elif options.field == 'url':
1376 url = cl.GetIssueURL()
1377 if url:
1378 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001379 return 0
1380
1381 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1382 if not branches:
1383 print('No local branch found.')
1384 return 0
1385
1386 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001387 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001388 alignment = max(5, max(len(b) for b in branches))
1389 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001390 # Adhoc thread pool to request data concurrently.
1391 output = Queue.Queue()
1392
1393 # Silence upload.py otherwise it becomes unweldly.
1394 upload.verbosity = 0
1395
1396 if not options.fast:
1397 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001398 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001399 c = Changelist(branchref=b)
1400 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001401 status = c.GetStatus()
1402 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001403
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001404 if i and (not status or status == 'error'):
1405 # The issue probably doesn't exist anymore.
1406 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001407
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001408 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001409
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001410 # Process one branch synchronously to work through authentication, then
1411 # spawn threads to process all the other branches in parallel.
1412 if branches:
1413 fetch(branches[0])
1414 threads = [
1415 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001416 for t in threads:
1417 t.daemon = True
1418 t.start()
1419 else:
1420 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1421 for b in branches:
1422 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001423 url = c.GetIssueURL()
1424 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001425
1426 tmp = {}
1427 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001428 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001429 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001430 b, i, color = output.get()
1431 tmp[b] = (i, color)
1432 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001433 reset = Fore.RESET
1434 if not sys.stdout.isatty():
1435 color = ''
1436 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001437 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001438 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001439
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001440 cl = Changelist()
1441 print
1442 print 'Current branch:',
1443 if not cl.GetIssue():
1444 print 'no issue assigned.'
1445 return 0
1446 print cl.GetBranch()
1447 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001448 if not options.fast:
1449 print 'Issue description:'
1450 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451 return 0
1452
1453
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001454def colorize_CMDstatus_doc():
1455 """To be called once in main() to add colors to git cl status help."""
1456 colors = [i for i in dir(Fore) if i[0].isupper()]
1457
1458 def colorize_line(line):
1459 for color in colors:
1460 if color in line.upper():
1461 # Extract whitespaces first and the leading '-'.
1462 indent = len(line) - len(line.lstrip(' ')) + 1
1463 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1464 return line
1465
1466 lines = CMDstatus.__doc__.splitlines()
1467 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1468
1469
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001470@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001472 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473
1474 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001475 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001476 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477
1478 cl = Changelist()
1479 if len(args) > 0:
1480 try:
1481 issue = int(args[0])
1482 except ValueError:
1483 DieWithError('Pass a number to set the issue or none to list it.\n'
1484 'Maybe you want to run git cl status?')
1485 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001486 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 return 0
1488
1489
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001490def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001491 """Shows or posts review comments for any changelist."""
1492 parser.add_option('-a', '--add-comment', dest='comment',
1493 help='comment to add to an issue')
1494 parser.add_option('-i', dest='issue',
1495 help="review issue id (defaults to current issue)")
1496 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001497
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001498 issue = None
1499 if options.issue:
1500 try:
1501 issue = int(options.issue)
1502 except ValueError:
1503 DieWithError('A review issue id is expected to be a number')
1504
1505 cl = Changelist(issue=issue)
1506
1507 if options.comment:
1508 cl.AddComment(options.comment)
1509 return 0
1510
1511 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001512 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001513 if message['disapproval']:
1514 color = Fore.RED
1515 elif message['approval']:
1516 color = Fore.GREEN
1517 elif message['sender'] == data['owner_email']:
1518 color = Fore.MAGENTA
1519 else:
1520 color = Fore.BLUE
1521 print '\n%s%s %s%s' % (
1522 color, message['date'].split('.', 1)[0], message['sender'],
1523 Fore.RESET)
1524 if message['text'].strip():
1525 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001526 return 0
1527
1528
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001529def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001530 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001531 cl = Changelist()
1532 if not cl.GetIssue():
1533 DieWithError('This branch has no associated changelist.')
1534 description = ChangeDescription(cl.GetDescription())
1535 description.prompt()
1536 cl.UpdateDescription(description.description)
1537 return 0
1538
1539
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540def CreateDescriptionFromLog(args):
1541 """Pulls out the commit log to use as a base for the CL description."""
1542 log_args = []
1543 if len(args) == 1 and not args[0].endswith('.'):
1544 log_args = [args[0] + '..']
1545 elif len(args) == 1 and args[0].endswith('...'):
1546 log_args = [args[0][:-1]]
1547 elif len(args) == 2:
1548 log_args = [args[0] + '..' + args[1]]
1549 else:
1550 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001551 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552
1553
thestig@chromium.org44202a22014-03-11 19:22:18 +00001554def CMDlint(parser, args):
1555 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001556 parser.add_option('--filter', action='append', metavar='-x,+y',
1557 help='Comma-separated list of cpplint\'s category-filters')
1558 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001559
1560 # Access to a protected member _XX of a client class
1561 # pylint: disable=W0212
1562 try:
1563 import cpplint
1564 import cpplint_chromium
1565 except ImportError:
1566 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1567 return 1
1568
1569 # Change the current working directory before calling lint so that it
1570 # shows the correct base.
1571 previous_cwd = os.getcwd()
1572 os.chdir(settings.GetRoot())
1573 try:
1574 cl = Changelist()
1575 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1576 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001577 if not files:
1578 print "Cannot lint an empty CL"
1579 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001580
1581 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001582 command = args + files
1583 if options.filter:
1584 command = ['--filter=' + ','.join(options.filter)] + command
1585 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001586
1587 white_regex = re.compile(settings.GetLintRegex())
1588 black_regex = re.compile(settings.GetLintIgnoreRegex())
1589 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1590 for filename in filenames:
1591 if white_regex.match(filename):
1592 if black_regex.match(filename):
1593 print "Ignoring file %s" % filename
1594 else:
1595 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1596 extra_check_functions)
1597 else:
1598 print "Skipping file %s" % filename
1599 finally:
1600 os.chdir(previous_cwd)
1601 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1602 if cpplint._cpplint_state.error_count != 0:
1603 return 1
1604 return 0
1605
1606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001608 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001609 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001611 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001612 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613 (options, args) = parser.parse_args(args)
1614
ukai@chromium.org259e4682012-10-25 07:36:33 +00001615 if not options.force and is_dirty_git_tree('presubmit'):
1616 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001617 return 1
1618
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001619 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001620 if args:
1621 base_branch = args[0]
1622 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001623 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001624 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001625
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001626 cl.RunHook(
1627 committing=not options.upload,
1628 may_prompt=False,
1629 verbose=options.verbose,
1630 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001631 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632
1633
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001634def AddChangeIdToCommitMessage(options, args):
1635 """Re-commits using the current message, assumes the commit hook is in
1636 place.
1637 """
1638 log_desc = options.message or CreateDescriptionFromLog(args)
1639 git_command = ['commit', '--amend', '-m', log_desc]
1640 RunGit(git_command)
1641 new_log_desc = CreateDescriptionFromLog(args)
1642 if CHANGE_ID in new_log_desc:
1643 print 'git-cl: Added Change-Id to commit message.'
1644 else:
1645 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1646
1647
piman@chromium.org336f9122014-09-04 02:16:55 +00001648def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001649 """upload the current branch to gerrit."""
1650 # We assume the remote called "origin" is the one we want.
1651 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001652 gerrit_remote = 'origin'
ukai@chromium.orge8077812012-02-03 03:41:46 +00001653 branch = 'master'
1654 if options.target_branch:
1655 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001657 change_desc = ChangeDescription(
1658 options.message or CreateDescriptionFromLog(args))
1659 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001660 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001661 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001662
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001663 if options.squash:
1664 # Try to get the message from a previous upload.
1665 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1666 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1667 if not message:
1668 if not options.force:
1669 change_desc.prompt()
1670
1671 if CHANGE_ID not in change_desc.description:
1672 # Run the commit-msg hook without modifying the head commit by writing
1673 # the commit message to a temporary file and running the hook over it,
1674 # then reading the file back in.
1675 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1676 'commit-msg')
1677 file_handle, msg_file = tempfile.mkstemp(text=True,
1678 prefix='commit_msg')
1679 try:
1680 try:
1681 with os.fdopen(file_handle, 'w') as fileobj:
1682 fileobj.write(change_desc.description)
1683 finally:
1684 os.close(file_handle)
1685 RunCommand([commit_msg_hook, msg_file])
1686 change_desc.set_description(gclient_utils.FileRead(msg_file))
1687 finally:
1688 os.remove(msg_file)
1689
1690 if not change_desc.description:
1691 print "Description is empty; aborting."
1692 return 1
1693
1694 message = change_desc.description
1695
1696 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1697 if remote is '.':
1698 # If our upstream branch is local, we base our squashed commit on its
1699 # squashed version.
1700 parent = ('refs/heads/git_cl_uploads/' +
1701 scm.GIT.ShortBranchName(upstream_branch))
1702
1703 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1704 # will create additional CLs when uploading.
1705 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1706 RunGitSilent(['rev-parse', parent + ':'])):
1707 print 'Upload upstream branch ' + upstream_branch + ' first.'
1708 return 1
1709 else:
1710 parent = cl.GetCommonAncestorWithUpstream()
1711
1712 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1713 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1714 '-m', message]).strip()
1715 else:
1716 if CHANGE_ID not in change_desc.description:
1717 AddChangeIdToCommitMessage(options, args)
1718 ref_to_push = 'HEAD'
1719 parent = '%s/%s' % (gerrit_remote, branch)
1720
1721 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1722 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001723 if len(commits) > 1:
1724 print('WARNING: This will upload %d commits. Run the following command '
1725 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001726 print('git log %s..%s' % (parent, ref_to_push))
1727 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001728 'commit.')
1729 ask_for_data('About to upload; enter to confirm.')
1730
piman@chromium.org336f9122014-09-04 02:16:55 +00001731 if options.reviewers or options.tbr_owners:
1732 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001733
ukai@chromium.orge8077812012-02-03 03:41:46 +00001734 receive_options = []
1735 cc = cl.GetCCList().split(',')
1736 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001737 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001738 cc = filter(None, cc)
1739 if cc:
1740 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001741 if change_desc.get_reviewers():
1742 receive_options.extend(
1743 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001744
ukai@chromium.orge8077812012-02-03 03:41:46 +00001745 git_command = ['push']
1746 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001747 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001748 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001749 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001750 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001751
1752 if options.squash:
1753 head = RunGit(['rev-parse', 'HEAD']).strip()
1754 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1755
ukai@chromium.orge8077812012-02-03 03:41:46 +00001756 # TODO(ukai): parse Change-Id: and set issue number?
1757 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759
wittman@chromium.org455dc922015-01-26 20:15:50 +00001760def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1761 """Computes the remote branch ref to use for the CL.
1762
1763 Args:
1764 remote (str): The git remote for the CL.
1765 remote_branch (str): The git remote branch for the CL.
1766 target_branch (str): The target branch specified by the user.
1767 pending_prefix (str): The pending prefix from the settings.
1768 """
1769 if not (remote and remote_branch):
1770 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001771
wittman@chromium.org455dc922015-01-26 20:15:50 +00001772 if target_branch:
1773 # Cannonicalize branch references to the equivalent local full symbolic
1774 # refs, which are then translated into the remote full symbolic refs
1775 # below.
1776 if '/' not in target_branch:
1777 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1778 else:
1779 prefix_replacements = (
1780 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1781 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1782 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1783 )
1784 match = None
1785 for regex, replacement in prefix_replacements:
1786 match = re.search(regex, target_branch)
1787 if match:
1788 remote_branch = target_branch.replace(match.group(0), replacement)
1789 break
1790 if not match:
1791 # This is a branch path but not one we recognize; use as-is.
1792 remote_branch = target_branch
1793 elif (not remote_branch.startswith('refs/remotes/branch-heads') and
1794 not remote_branch.startswith('refs/remotes/%s/refs' % remote)):
1795 # Default to master for refs that are not branches.
1796 remote_branch = 'refs/remotes/%s/master' % remote
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001797
wittman@chromium.org455dc922015-01-26 20:15:50 +00001798 # Create the true path to the remote branch.
1799 # Does the following translation:
1800 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1801 # * refs/remotes/origin/master -> refs/heads/master
1802 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1803 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1804 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1805 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1806 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1807 'refs/heads/')
1808 elif remote_branch.startswith('refs/remotes/branch-heads'):
1809 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1810 # If a pending prefix exists then replace refs/ with it.
1811 if pending_prefix:
1812 remote_branch = remote_branch.replace('refs/', pending_prefix)
1813 return remote_branch
1814
1815
piman@chromium.org336f9122014-09-04 02:16:55 +00001816def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001817 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1819 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820 if options.emulate_svn_auto_props:
1821 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001822
1823 change_desc = None
1824
pgervais@chromium.org91141372014-01-09 23:27:20 +00001825 if options.email is not None:
1826 upload_args.extend(['--email', options.email])
1827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001829 if options.title:
1830 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001831 if options.message:
1832 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001833 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834 print ("This branch is associated with issue %s. "
1835 "Adding patch to that issue." % cl.GetIssue())
1836 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001837 if options.title:
1838 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001839 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001840 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001841 if options.reviewers or options.tbr_owners:
1842 change_desc.update_reviewers(options.reviewers,
1843 options.tbr_owners,
1844 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001845 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001846 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001847
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001848 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 print "Description is empty; aborting."
1850 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001851
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001852 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001853 if change_desc.get_reviewers():
1854 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001855 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001856 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001857 DieWithError("Must specify reviewers to send email.")
1858 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001859
1860 # We check this before applying rietveld.private assuming that in
1861 # rietveld.cc only addresses which we can send private CLs to are listed
1862 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1863 # --private is specified explicitly on the command line.
1864 if options.private:
1865 logging.warn('rietveld.cc is ignored since private flag is specified. '
1866 'You need to review and add them manually if necessary.')
1867 cc = cl.GetCCListWithoutDefault()
1868 else:
1869 cc = cl.GetCCList()
1870 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001871 if cc:
1872 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001873
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001874 if options.private or settings.GetDefaultPrivateFlag() == "True":
1875 upload_args.append('--private')
1876
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001877 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001878 if not options.find_copies:
1879 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 # Include the upstream repo's URL in the change -- this is useful for
1882 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001883 remote_url = cl.GetGitBaseUrlFromConfig()
1884 if not remote_url:
1885 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001886 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001887 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001888 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1889 remote_url = (cl.GetRemoteUrl() + '@'
1890 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001891 if remote_url:
1892 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00001893 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00001894 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1895 settings.GetPendingRefPrefix())
1896 if target_ref:
1897 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001899 project = settings.GetProject()
1900 if project:
1901 upload_args.extend(['--project', project])
1902
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001904 upload_args = ['upload'] + upload_args + args
1905 logging.info('upload.RealMain(%s)', upload_args)
1906 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001907 issue = int(issue)
1908 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001909 except KeyboardInterrupt:
1910 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911 except:
1912 # If we got an exception after the user typed a description for their
1913 # change, back up the description before re-raising.
1914 if change_desc:
1915 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1916 print '\nGot exception while uploading -- saving description to %s\n' \
1917 % backup_path
1918 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001919 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920 backup_file.close()
1921 raise
1922
1923 if not cl.GetIssue():
1924 cl.SetIssue(issue)
1925 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001926
1927 if options.use_commit_queue:
1928 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929 return 0
1930
1931
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001932def cleanup_list(l):
1933 """Fixes a list so that comma separated items are put as individual items.
1934
1935 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1936 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1937 """
1938 items = sum((i.split(',') for i in l), [])
1939 stripped_items = (i.strip() for i in items)
1940 return sorted(filter(None, stripped_items))
1941
1942
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001943@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001944def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001945 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001946 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1947 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001948 parser.add_option('--bypass-watchlists', action='store_true',
1949 dest='bypass_watchlists',
1950 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001951 parser.add_option('-f', action='store_true', dest='force',
1952 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001953 parser.add_option('-m', dest='message', help='message for patchset')
1954 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001955 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001956 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001957 help='reviewer email addresses')
1958 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001959 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001960 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001961 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001962 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001963 parser.add_option('--emulate_svn_auto_props',
1964 '--emulate-svn-auto-props',
1965 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001966 dest="emulate_svn_auto_props",
1967 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001968 parser.add_option('-c', '--use-commit-queue', action='store_true',
1969 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001970 parser.add_option('--private', action='store_true',
1971 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001972 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001973 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00001974 metavar='TARGET',
1975 help='Apply CL to remote ref TARGET. ' +
1976 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001977 parser.add_option('--squash', action='store_true',
1978 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001979 parser.add_option('--email', default=None,
1980 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001981 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1982 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001983
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001984 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001985 (options, args) = parser.parse_args(args)
1986
ukai@chromium.org259e4682012-10-25 07:36:33 +00001987 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001988 return 1
1989
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001990 options.reviewers = cleanup_list(options.reviewers)
1991 options.cc = cleanup_list(options.cc)
1992
ukai@chromium.orge8077812012-02-03 03:41:46 +00001993 cl = Changelist()
1994 if args:
1995 # TODO(ukai): is it ok for gerrit case?
1996 base_branch = args[0]
1997 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00001998 if cl.GetBranch() is None:
1999 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2000
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002001 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002002 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002003 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002004
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002005 # Apply watchlists on upload.
2006 change = cl.GetChange(base_branch, None)
2007 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2008 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002009 if not options.bypass_watchlists:
2010 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002011
ukai@chromium.orge8077812012-02-03 03:41:46 +00002012 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002013 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002014 # Set the reviewer list now so that presubmit checks can access it.
2015 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002016 change_description.update_reviewers(options.reviewers,
2017 options.tbr_owners,
2018 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002019 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002020 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002021 may_prompt=not options.force,
2022 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002023 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002024 if not hook_results.should_continue():
2025 return 1
2026 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002027 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002028
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002029 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002030 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002031 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002032 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002033 print ('The last upload made from this repository was patchset #%d but '
2034 'the most recent patchset on the server is #%d.'
2035 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002036 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2037 'from another machine or branch the patch you\'re uploading now '
2038 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002039 ask_for_data('About to upload; enter to confirm.')
2040
iannucci@chromium.org79540052012-10-19 23:15:26 +00002041 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002042 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002043 return GerritUpload(options, args, cl, change)
2044 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002045 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002046 git_set_branch_value('last-upload-hash',
2047 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002048 # Run post upload hooks, if specified.
2049 if settings.GetRunPostUploadHook():
2050 presubmit_support.DoPostUploadExecuter(
2051 change,
2052 cl,
2053 settings.GetRoot(),
2054 options.verbose,
2055 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002056
2057 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002058
2059
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002060def IsSubmoduleMergeCommit(ref):
2061 # When submodules are added to the repo, we expect there to be a single
2062 # non-git-svn merge commit at remote HEAD with a signature comment.
2063 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002064 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002065 return RunGit(cmd) != ''
2066
2067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002069 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002071 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072 Updates changelog with metadata (e.g. pointer to review).
2073 Pushes/dcommits the code upstream.
2074 Updates review and closes.
2075 """
2076 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2077 help='bypass upload presubmit hook')
2078 parser.add_option('-m', dest='message',
2079 help="override review description")
2080 parser.add_option('-f', action='store_true', dest='force',
2081 help="force yes to questions (don't prompt)")
2082 parser.add_option('-c', dest='contributor',
2083 help="external contributor for patch (appended to " +
2084 "description and used as author for git). Should be " +
2085 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002086 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002087 (options, args) = parser.parse_args(args)
2088 cl = Changelist()
2089
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002090 current = cl.GetBranch()
2091 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2092 if not settings.GetIsGitSvn() and remote == '.':
2093 print
2094 print 'Attempting to push branch %r into another local branch!' % current
2095 print
2096 print 'Either reparent this branch on top of origin/master:'
2097 print ' git reparent-branch --root'
2098 print
2099 print 'OR run `git rebase-update` if you think the parent branch is already'
2100 print 'committed.'
2101 print
2102 print ' Current parent: %r' % upstream_branch
2103 return 1
2104
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002105 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 # Default to merging against our best guess of the upstream branch.
2107 args = [cl.GetUpstreamBranch()]
2108
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002109 if options.contributor:
2110 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2111 print "Please provide contibutor as 'First Last <email@example.com>'"
2112 return 1
2113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002114 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002115 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002116
ukai@chromium.org259e4682012-10-25 07:36:33 +00002117 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 return 1
2119
2120 # This rev-list syntax means "show all commits not in my branch that
2121 # are in base_branch".
2122 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2123 base_branch]).splitlines()
2124 if upstream_commits:
2125 print ('Base branch "%s" has %d commits '
2126 'not in this branch.' % (base_branch, len(upstream_commits)))
2127 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2128 return 1
2129
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002130 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002131 svn_head = None
2132 if cmd == 'dcommit' or base_has_submodules:
2133 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2134 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002135
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002137 # If the base_head is a submodule merge commit, the first parent of the
2138 # base_head should be a git-svn commit, which is what we're interested in.
2139 base_svn_head = base_branch
2140 if base_has_submodules:
2141 base_svn_head += '^1'
2142
2143 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002144 if extra_commits:
2145 print ('This branch has %d additional commits not upstreamed yet.'
2146 % len(extra_commits.splitlines()))
2147 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2148 'before attempting to %s.' % (base_branch, cmd))
2149 return 1
2150
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002151 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002152 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002153 author = None
2154 if options.contributor:
2155 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002156 hook_results = cl.RunHook(
2157 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002158 may_prompt=not options.force,
2159 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002160 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002161 if not hook_results.should_continue():
2162 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002163
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002164 # Check the tree status if the tree status URL is set.
2165 status = GetTreeStatus()
2166 if 'closed' == status:
2167 print('The tree is closed. Please wait for it to reopen. Use '
2168 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2169 return 1
2170 elif 'unknown' == status:
2171 print('Unable to determine tree status. Please verify manually and '
2172 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2173 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002174 else:
2175 breakpad.SendStack(
2176 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002177 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2178 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002179 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002180
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002181 change_desc = ChangeDescription(options.message)
2182 if not change_desc.description and cl.GetIssue():
2183 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002184
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002185 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002186 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002187 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002188 else:
2189 print 'No description set.'
2190 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2191 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002193 # Keep a separate copy for the commit message, because the commit message
2194 # contains the link to the Rietveld issue, while the Rietveld message contains
2195 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002196 # Keep a separate copy for the commit message.
2197 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002198 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002199
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002200 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002201 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002202 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002204 commit_desc.append_footer('Patch from %s.' % options.contributor)
2205
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002206 print('Description:')
2207 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002208
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002209 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002210 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002211 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002212
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002213 # We want to squash all this branch's commits into one commit with the proper
2214 # description. We do this by doing a "reset --soft" to the base branch (which
2215 # keeps the working copy the same), then dcommitting that. If origin/master
2216 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2217 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002218 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002219 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2220 # Delete the branches if they exist.
2221 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2222 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2223 result = RunGitWithCode(showref_cmd)
2224 if result[0] == 0:
2225 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002226
2227 # We might be in a directory that's present in this branch but not in the
2228 # trunk. Move up to the top of the tree so that git commands that expect a
2229 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002230 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002231 if rel_base_path:
2232 os.chdir(rel_base_path)
2233
2234 # Stuff our change into the merge branch.
2235 # We wrap in a try...finally block so if anything goes wrong,
2236 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002237 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002238 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002239 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002240 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002241 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002242 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002243 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002244 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002245 RunGit(
2246 [
2247 'commit', '--author', options.contributor,
2248 '-m', commit_desc.description,
2249 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002250 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002251 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002252 if base_has_submodules:
2253 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2254 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2255 RunGit(['checkout', CHERRY_PICK_BRANCH])
2256 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002257 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002258 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002259 pending_prefix = settings.GetPendingRefPrefix()
2260 if not pending_prefix or branch.startswith(pending_prefix):
2261 # If not using refs/pending/heads/* at all, or target ref is already set
2262 # to pending, then push to the target ref directly.
2263 retcode, output = RunGitWithCode(
2264 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002265 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002266 else:
2267 # Cherry-pick the change on top of pending ref and then push it.
2268 assert branch.startswith('refs/'), branch
2269 assert pending_prefix[-1] == '/', pending_prefix
2270 pending_ref = pending_prefix + branch[len('refs/'):]
2271 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002272 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002273 if retcode == 0:
2274 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002275 else:
2276 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002277 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002278 'svn', 'dcommit',
2279 '-C%s' % options.similarity,
2280 '--no-rebase', '--rmdir',
2281 ]
2282 if settings.GetForceHttpsCommitUrl():
2283 # Allow forcing https commit URLs for some projects that don't allow
2284 # committing to http URLs (like Google Code).
2285 remote_url = cl.GetGitSvnRemoteUrl()
2286 if urlparse.urlparse(remote_url).scheme == 'http':
2287 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002288 cmd_args.append('--commit-url=%s' % remote_url)
2289 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002290 if 'Committed r' in output:
2291 revision = re.match(
2292 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2293 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002294 finally:
2295 # And then swap back to the original branch and clean up.
2296 RunGit(['checkout', '-q', cl.GetBranch()])
2297 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002298 if base_has_submodules:
2299 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002300
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002301 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002302 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002303 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002304
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002305 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002306 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002307 try:
2308 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2309 # We set pushed_to_pending to False, since it made it all the way to the
2310 # real ref.
2311 pushed_to_pending = False
2312 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002313 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002315 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002316 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002317 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002318 if not to_pending:
2319 if viewvc_url and revision:
2320 change_desc.append_footer(
2321 'Committed: %s%s' % (viewvc_url, revision))
2322 elif revision:
2323 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002324 print ('Closing issue '
2325 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002326 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002328 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002329 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002330 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002331 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002332 if options.bypass_hooks:
2333 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2334 else:
2335 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002336 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002337 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002338
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002339 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002340 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2341 print 'The commit is in the pending queue (%s).' % pending_ref
2342 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002343 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002344 'footer.' % branch)
2345
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002346 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2347 if os.path.isfile(hook):
2348 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002349
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002350 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002351
2352
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002353def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2354 print
2355 print 'Waiting for commit to be landed on %s...' % real_ref
2356 print '(If you are impatient, you may Ctrl-C once without harm)'
2357 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2358 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2359
2360 loop = 0
2361 while True:
2362 sys.stdout.write('fetching (%d)... \r' % loop)
2363 sys.stdout.flush()
2364 loop += 1
2365
2366 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2367 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2368 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2369 for commit in commits.splitlines():
2370 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2371 print 'Found commit on %s' % real_ref
2372 return commit
2373
2374 current_rev = to_rev
2375
2376
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002377def PushToGitPending(remote, pending_ref, upstream_ref):
2378 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2379
2380 Returns:
2381 (retcode of last operation, output log of last operation).
2382 """
2383 assert pending_ref.startswith('refs/'), pending_ref
2384 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2385 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2386 code = 0
2387 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002388 max_attempts = 3
2389 attempts_left = max_attempts
2390 while attempts_left:
2391 if attempts_left != max_attempts:
2392 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2393 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002394
2395 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002396 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002397 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002398 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002399 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002400 print 'Fetch failed with exit code %d.' % code
2401 if out.strip():
2402 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002403 continue
2404
2405 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002406 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002407 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002408 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002409 if code:
2410 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002411 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2412 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002413 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2414 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002415 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002416 return code, out
2417
2418 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002419 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002420 code, out = RunGitWithCode(
2421 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2422 if code == 0:
2423 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002424 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002425 return code, out
2426
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002427 print 'Push failed with exit code %d.' % code
2428 if out.strip():
2429 print out.strip()
2430 if IsFatalPushFailure(out):
2431 print (
2432 'Fatal push error. Make sure your .netrc credentials and git '
2433 'user.email are correct and you have push access to the repo.')
2434 return code, out
2435
2436 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002437 return code, out
2438
2439
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002440def IsFatalPushFailure(push_stdout):
2441 """True if retrying push won't help."""
2442 return '(prohibited by Gerrit)' in push_stdout
2443
2444
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002445@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002446def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002447 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002448 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002449 message = """This doesn't appear to be an SVN repository.
2450If your project has a git mirror with an upstream SVN master, you probably need
2451to run 'git svn init', see your project's git mirror documentation.
2452If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002453to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002454Choose wisely, if you get this wrong, your commit might appear to succeed but
2455will instead be silently ignored."""
2456 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002457 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002458 return SendUpstream(parser, args, 'dcommit')
2459
2460
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002461@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002462def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002463 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002464 if settings.GetIsGitSvn():
2465 print('This appears to be an SVN repository.')
2466 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002467 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002468 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002469
2470
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002471@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002472def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002473 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474 parser.add_option('-b', dest='newbranch',
2475 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002476 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002477 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002478 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2479 help='Change to the directory DIR immediately, '
2480 'before doing anything else.')
2481 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002482 help='failed patches spew .rej files rather than '
2483 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002484 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2485 help="don't commit after patch applies")
2486 (options, args) = parser.parse_args(args)
2487 if len(args) != 1:
2488 parser.print_help()
2489 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002490 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002491
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002492 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002493 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002494
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002495 if options.newbranch:
2496 if options.force:
2497 RunGit(['branch', '-D', options.newbranch],
2498 stderr=subprocess2.PIPE, error_ok=True)
2499 RunGit(['checkout', '-b', options.newbranch,
2500 Changelist().GetUpstreamBranch()])
2501
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002502 return PatchIssue(issue_arg, options.reject, options.nocommit,
2503 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002504
2505
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002506def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002507 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002508 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002509 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002510 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002511 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002512 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002513 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002514 # Assume it's a URL to the patch. Default to https.
2515 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002516 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002517 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002518 DieWithError('Must pass an issue ID or full URL for '
2519 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002520 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002521 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002522 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002524 # Switch up to the top-level directory, if necessary, in preparation for
2525 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002526 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002527 if top:
2528 os.chdir(top)
2529
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002530 # Git patches have a/ at the beginning of source paths. We strip that out
2531 # with a sed script rather than the -p flag to patch so we can feed either
2532 # Git or svn-style patches into the same apply command.
2533 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002534 try:
2535 patch_data = subprocess2.check_output(
2536 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2537 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002538 DieWithError('Git patch mungling failed.')
2539 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002540
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002541 # We use "git apply" to apply the patch instead of "patch" so that we can
2542 # pick up file adds.
2543 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002544 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002545 if directory:
2546 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002547 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002548 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002549 elif IsGitVersionAtLeast('1.7.12'):
2550 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002551 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002552 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002553 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002554 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002555 DieWithError('Failed to apply the patch')
2556
2557 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002558 if not nocommit:
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002559 RunGit(['commit', '-m', ('patch from issue %(i)s at patchset '
2560 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2561 % {'i': issue, 'p': patchset})])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002562 cl = Changelist()
2563 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002564 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002565 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002566 else:
2567 print "Patch applied to index."
2568 return 0
2569
2570
2571def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002572 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002573 # Provide a wrapper for git svn rebase to help avoid accidental
2574 # git svn dcommit.
2575 # It's the only command that doesn't use parser at all since we just defer
2576 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002577
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002578 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002579
2580
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002581def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002582 """Fetches the tree status and returns either 'open', 'closed',
2583 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002584 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002585 if url:
2586 status = urllib2.urlopen(url).read().lower()
2587 if status.find('closed') != -1 or status == '0':
2588 return 'closed'
2589 elif status.find('open') != -1 or status == '1':
2590 return 'open'
2591 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002592 return 'unset'
2593
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002594
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002595def GetTreeStatusReason():
2596 """Fetches the tree status from a json url and returns the message
2597 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002598 url = settings.GetTreeStatusUrl()
2599 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002600 connection = urllib2.urlopen(json_url)
2601 status = json.loads(connection.read())
2602 connection.close()
2603 return status['message']
2604
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002605
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002606def GetBuilderMaster(bot_list):
2607 """For a given builder, fetch the master from AE if available."""
2608 map_url = 'https://builders-map.appspot.com/'
2609 try:
2610 master_map = json.load(urllib2.urlopen(map_url))
2611 except urllib2.URLError as e:
2612 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2613 (map_url, e))
2614 except ValueError as e:
2615 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2616 if not master_map:
2617 return None, 'Failed to build master map.'
2618
2619 result_master = ''
2620 for bot in bot_list:
2621 builder = bot.split(':', 1)[0]
2622 master_list = master_map.get(builder, [])
2623 if not master_list:
2624 return None, ('No matching master for builder %s.' % builder)
2625 elif len(master_list) > 1:
2626 return None, ('The builder name %s exists in multiple masters %s.' %
2627 (builder, master_list))
2628 else:
2629 cur_master = master_list[0]
2630 if not result_master:
2631 result_master = cur_master
2632 elif result_master != cur_master:
2633 return None, 'The builders do not belong to the same master.'
2634 return result_master, None
2635
2636
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002637def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002638 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002639 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640 status = GetTreeStatus()
2641 if 'unset' == status:
2642 print 'You must configure your tree status URL by running "git cl config".'
2643 return 2
2644
2645 print "The tree is %s" % status
2646 print
2647 print GetTreeStatusReason()
2648 if status != 'open':
2649 return 1
2650 return 0
2651
2652
maruel@chromium.org15192402012-09-06 12:38:29 +00002653def CMDtry(parser, args):
2654 """Triggers a try job through Rietveld."""
2655 group = optparse.OptionGroup(parser, "Try job options")
2656 group.add_option(
2657 "-b", "--bot", action="append",
2658 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2659 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002660 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002661 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002662 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002663 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002664 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002665 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002666 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002667 "-r", "--revision",
2668 help="Revision to use for the try job; default: the "
2669 "revision will be determined by the try server; see "
2670 "its waterfall for more info")
2671 group.add_option(
2672 "-c", "--clobber", action="store_true", default=False,
2673 help="Force a clobber before building; e.g. don't do an "
2674 "incremental build")
2675 group.add_option(
2676 "--project",
2677 help="Override which project to use. Projects are defined "
2678 "server-side to define what default bot set to use")
2679 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002680 "-n", "--name", help="Try job name; default to current branch name")
2681 parser.add_option_group(group)
2682 options, args = parser.parse_args(args)
2683
2684 if args:
2685 parser.error('Unknown arguments: %s' % args)
2686
2687 cl = Changelist()
2688 if not cl.GetIssue():
2689 parser.error('Need to upload first')
2690
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002691 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002692 if props.get('closed'):
2693 parser.error('Cannot send tryjobs for a closed CL')
2694
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002695 if props.get('private'):
2696 parser.error('Cannot use trybots with private issue')
2697
maruel@chromium.org15192402012-09-06 12:38:29 +00002698 if not options.name:
2699 options.name = cl.GetBranch()
2700
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002701 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002702 options.master, err_msg = GetBuilderMaster(options.bot)
2703 if err_msg:
2704 parser.error('Tryserver master cannot be found because: %s\n'
2705 'Please manually specify the tryserver master'
2706 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002707
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002708 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002709 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002710 if not options.bot:
2711 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002712
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002713 # Get try masters from PRESUBMIT.py files.
2714 masters = presubmit_support.DoGetTryMasters(
2715 change,
2716 change.LocalPaths(),
2717 settings.GetRoot(),
2718 None,
2719 None,
2720 options.verbose,
2721 sys.stdout)
2722 if masters:
2723 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002724
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002725 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2726 options.bot = presubmit_support.DoGetTrySlaves(
2727 change,
2728 change.LocalPaths(),
2729 settings.GetRoot(),
2730 None,
2731 None,
2732 options.verbose,
2733 sys.stdout)
2734 if not options.bot:
2735 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002736
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002737 builders_and_tests = {}
2738 # TODO(machenbach): The old style command-line options don't support
2739 # multiple try masters yet.
2740 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2741 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2742
2743 for bot in old_style:
2744 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002745 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002746 elif ',' in bot:
2747 parser.error('Specify one bot per --bot flag')
2748 else:
2749 builders_and_tests.setdefault(bot, []).append('defaulttests')
2750
2751 for bot, tests in new_style:
2752 builders_and_tests.setdefault(bot, []).extend(tests)
2753
2754 # Return a master map with one master to be backwards compatible. The
2755 # master name defaults to an empty string, which will cause the master
2756 # not to be set on rietveld (deprecated).
2757 return {options.master: builders_and_tests}
2758
2759 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002760
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002761 for builders in masters.itervalues():
2762 if any('triggered' in b for b in builders):
2763 print >> sys.stderr, (
2764 'ERROR You are trying to send a job to a triggered bot. This type of'
2765 ' bot requires an\ninitial job from a parent (usually a builder). '
2766 'Instead send your job to the parent.\n'
2767 'Bot list: %s' % builders)
2768 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002769
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002770 patchset = cl.GetMostRecentPatchset()
2771 if patchset and patchset != cl.GetPatchset():
2772 print(
2773 '\nWARNING Mismatch between local config and server. Did a previous '
2774 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2775 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002776 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002777 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002778 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002779 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002780 except urllib2.HTTPError, e:
2781 if e.code == 404:
2782 print('404 from rietveld; '
2783 'did you mean to use "git try" instead of "git cl try"?')
2784 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002785 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002786
2787 for (master, builders) in masters.iteritems():
2788 if master:
2789 print 'Master: %s' % master
2790 length = max(len(builder) for builder in builders)
2791 for builder in sorted(builders):
2792 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002793 return 0
2794
2795
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002796@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002798 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002799 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002800 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002801 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002804 if args:
2805 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002806 branch = cl.GetBranch()
2807 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002808 cl = Changelist()
2809 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002810
2811 # Clear configured merge-base, if there is one.
2812 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002813 else:
2814 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002815 return 0
2816
2817
thestig@chromium.org00858c82013-12-02 23:08:03 +00002818def CMDweb(parser, args):
2819 """Opens the current CL in the web browser."""
2820 _, args = parser.parse_args(args)
2821 if args:
2822 parser.error('Unrecognized args: %s' % ' '.join(args))
2823
2824 issue_url = Changelist().GetIssueURL()
2825 if not issue_url:
2826 print >> sys.stderr, 'ERROR No issue to open'
2827 return 1
2828
2829 webbrowser.open(issue_url)
2830 return 0
2831
2832
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002833def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002834 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002835 _, args = parser.parse_args(args)
2836 if args:
2837 parser.error('Unrecognized args: %s' % ' '.join(args))
2838 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002839 props = cl.GetIssueProperties()
2840 if props.get('private'):
2841 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002842 cl.SetFlag('commit', '1')
2843 return 0
2844
2845
groby@chromium.org411034a2013-02-26 15:12:01 +00002846def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002847 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002848 _, args = parser.parse_args(args)
2849 if args:
2850 parser.error('Unrecognized args: %s' % ' '.join(args))
2851 cl = Changelist()
2852 # Ensure there actually is an issue to close.
2853 cl.GetDescription()
2854 cl.CloseIssue()
2855 return 0
2856
2857
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002858def CMDdiff(parser, args):
2859 """shows differences between local tree and last upload."""
2860 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002861 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002862 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002863 if not issue:
2864 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002865 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002866 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002867
2868 # Create a new branch based on the merge-base
2869 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2870 try:
2871 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002872 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002873 if rtn != 0:
2874 return rtn
2875
wychen@chromium.org06928532015-02-03 02:11:29 +00002876 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002877 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00002878 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002879 finally:
2880 RunGit(['checkout', '-q', branch])
2881 RunGit(['branch', '-D', TMP_BRANCH])
2882
2883 return 0
2884
2885
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002886def CMDowners(parser, args):
2887 """interactively find the owners for reviewing"""
2888 parser.add_option(
2889 '--no-color',
2890 action='store_true',
2891 help='Use this option to disable color output')
2892 options, args = parser.parse_args(args)
2893
2894 author = RunGit(['config', 'user.email']).strip() or None
2895
2896 cl = Changelist()
2897
2898 if args:
2899 if len(args) > 1:
2900 parser.error('Unknown args')
2901 base_branch = args[0]
2902 else:
2903 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002904 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002905
2906 change = cl.GetChange(base_branch, None)
2907 return owners_finder.OwnersFinder(
2908 [f.LocalPath() for f in
2909 cl.GetChange(base_branch, None).AffectedFiles()],
2910 change.RepositoryRoot(), author,
2911 fopen=file, os_path=os.path, glob=glob.glob,
2912 disable_color=options.no_color).run()
2913
2914
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002915def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
2916 """Generates a diff command."""
2917 # Generate diff for the current branch's changes.
2918 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
2919 upstream_commit, '--' ]
2920
2921 if args:
2922 for arg in args:
2923 if os.path.isdir(arg):
2924 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
2925 elif os.path.isfile(arg):
2926 diff_cmd.append(arg)
2927 else:
2928 DieWithError('Argument "%s" is not a file or a directory' % arg)
2929 else:
2930 diff_cmd.extend('*' + ext for ext in extensions)
2931
2932 return diff_cmd
2933
2934
enne@chromium.org555cfe42014-01-29 18:21:39 +00002935@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002936def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002937 """Runs clang-format on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00002938 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002939 parser.add_option('--full', action='store_true',
2940 help='Reformat the full content of all touched files')
2941 parser.add_option('--dry-run', action='store_true',
2942 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002943 parser.add_option('--diff', action='store_true',
2944 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002945 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002946
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002947 # git diff generates paths against the root of the repository. Change
2948 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002949 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002950 if rel_base_path:
2951 os.chdir(rel_base_path)
2952
digit@chromium.org29e47272013-05-17 17:01:46 +00002953 # Grab the merge-base commit, i.e. the upstream commit of the current
2954 # branch when it was created or the last time it was rebased. This is
2955 # to cover the case where the user may have called "git fetch origin",
2956 # moving the origin branch to a newer commit, but hasn't rebased yet.
2957 upstream_commit = None
2958 cl = Changelist()
2959 upstream_branch = cl.GetUpstreamBranch()
2960 if upstream_branch:
2961 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2962 upstream_commit = upstream_commit.strip()
2963
2964 if not upstream_commit:
2965 DieWithError('Could not find base commit for this branch. '
2966 'Are you in detached state?')
2967
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002968 if opts.full:
2969 # Only list the names of modified files.
2970 clang_diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00002971 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002972 # Only generate context-less patches.
2973 clang_diff_type = '-U0'
2974
2975 diff_cmd = BuildGitDiffCmd(clang_diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00002976 diff_output = RunGit(diff_cmd)
2977
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002978 top_dir = os.path.normpath(
2979 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2980
2981 # Locate the clang-format binary in the checkout
2982 try:
2983 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2984 except clang_format.NotFoundError, e:
2985 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002986
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002987 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
2988 # formatted. This is used to block during the presubmit.
2989 return_value = 0
2990
digit@chromium.org29e47272013-05-17 17:01:46 +00002991 if opts.full:
2992 # diff_output is a list of files to send to clang-format.
2993 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002994 if files:
2995 cmd = [clang_format_tool]
2996 if not opts.dry_run and not opts.diff:
2997 cmd.append('-i')
2998 stdout = RunCommand(cmd + files, cwd=top_dir)
2999 if opts.diff:
3000 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003001 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003002 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003003 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003004 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003005 try:
3006 script = clang_format.FindClangFormatScriptInChromiumTree(
3007 'clang-format-diff.py')
3008 except clang_format.NotFoundError, e:
3009 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003010
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003011 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003012 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003013 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003014
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003015 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003016 if opts.diff:
3017 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003018 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003019 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003020
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003021 # Build a diff command that only operates on dart files. dart's formatter
3022 # does not have the nice property of only operating on modified chunks, so
3023 # hard code full.
3024 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3025 args, ['.dart'])
3026 dart_diff_output = RunGit(dart_diff_cmd)
3027 if dart_diff_output:
3028 try:
3029 command = [dart_format.FindDartFmtToolInChromiumTree()]
3030 if not opts.dry_run and not opts.diff:
3031 command.append('-w')
3032 command.extend(dart_diff_output.splitlines())
3033
3034 stdout = RunCommand(command, cwd=top_dir, env=env)
3035 if opts.dry_run and stdout:
3036 return_value = 2
3037 except dart_format.NotFoundError as e:
3038 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3039 'this checkout.')
3040
3041 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003042
3043
maruel@chromium.org29404b52014-09-08 22:58:00 +00003044def CMDlol(parser, args):
3045 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003046 print zlib.decompress(base64.b64decode(
3047 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3048 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3049 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3050 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003051 return 0
3052
3053
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003054class OptionParser(optparse.OptionParser):
3055 """Creates the option parse and add --verbose support."""
3056 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003057 optparse.OptionParser.__init__(
3058 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003059 self.add_option(
3060 '-v', '--verbose', action='count', default=0,
3061 help='Use 2 times for more debugging info')
3062
3063 def parse_args(self, args=None, values=None):
3064 options, args = optparse.OptionParser.parse_args(self, args, values)
3065 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3066 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3067 return options, args
3068
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003070def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003071 if sys.hexversion < 0x02060000:
3072 print >> sys.stderr, (
3073 '\nYour python version %s is unsupported, please upgrade.\n' %
3074 sys.version.split(' ', 1)[0])
3075 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003076
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003077 # Reload settings.
3078 global settings
3079 settings = Settings()
3080
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003081 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003082 dispatcher = subcommand.CommandDispatcher(__name__)
3083 try:
3084 return dispatcher.execute(OptionParser(), argv)
3085 except urllib2.HTTPError, e:
3086 if e.code != 500:
3087 raise
3088 DieWithError(
3089 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3090 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003091
3092
3093if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003094 # These affect sys.stdout so do it outside of main() to simplify mocks in
3095 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003096 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003097 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098 sys.exit(main(sys.argv[1:]))