blob: 728bae7abcf41c980e58db900554f3c9fbb58fe4 [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
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000435 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000436 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000437
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000438 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000439 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000440
ukai@chromium.orge8077812012-02-03 03:41:46 +0000441 def GetIsGerrit(self):
442 """Return true if this repo is assosiated with gerrit code review system."""
443 if self.is_gerrit is None:
444 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
445 return self.is_gerrit
446
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000447 def GetGitEditor(self):
448 """Return the editor specified in the git config, or None if none is."""
449 if self.git_editor is None:
450 self.git_editor = self._GetConfig('core.editor', error_ok=True)
451 return self.git_editor or None
452
thestig@chromium.org44202a22014-03-11 19:22:18 +0000453 def GetLintRegex(self):
454 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
455 DEFAULT_LINT_REGEX)
456
457 def GetLintIgnoreRegex(self):
458 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
459 DEFAULT_LINT_IGNORE_REGEX)
460
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000461 def GetProject(self):
462 if not self.project:
463 self.project = self._GetRietveldConfig('project', error_ok=True)
464 return self.project
465
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000466 def GetForceHttpsCommitUrl(self):
467 if not self.force_https_commit_url:
468 self.force_https_commit_url = self._GetRietveldConfig(
469 'force-https-commit-url', error_ok=True)
470 return self.force_https_commit_url
471
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000472 def GetPendingRefPrefix(self):
473 if not self.pending_ref_prefix:
474 self.pending_ref_prefix = self._GetRietveldConfig(
475 'pending-ref-prefix', error_ok=True)
476 return self.pending_ref_prefix
477
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000478 def _GetRietveldConfig(self, param, **kwargs):
479 return self._GetConfig('rietveld.' + param, **kwargs)
480
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 def _GetConfig(self, param, **kwargs):
482 self.LazyUpdateIfNeeded()
483 return RunGit(['config', param], **kwargs).strip()
484
485
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000486def ShortBranchName(branch):
487 """Convert a name like 'refs/heads/foo' to just 'foo'."""
488 return branch.replace('refs/heads/', '')
489
490
491class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000492 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000494 global settings
495 if not settings:
496 # Happens when git_cl.py is used as a utility library.
497 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000498 settings.GetDefaultServerUrl()
499 self.branchref = branchref
500 if self.branchref:
501 self.branch = ShortBranchName(self.branchref)
502 else:
503 self.branch = None
504 self.rietveld_server = None
505 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000506 self.lookedup_issue = False
507 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000508 self.has_description = False
509 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000510 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000511 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000512 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000513 self.cc = None
514 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000515 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000516 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000517
518 def GetCCList(self):
519 """Return the users cc'd on this CL.
520
521 Return is a string suitable for passing to gcl with the --cc flag.
522 """
523 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000524 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000525 more_cc = ','.join(self.watchers)
526 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
527 return self.cc
528
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000529 def GetCCListWithoutDefault(self):
530 """Return the users cc'd on this CL excluding default ones."""
531 if self.cc is None:
532 self.cc = ','.join(self.watchers)
533 return self.cc
534
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000535 def SetWatchers(self, watchers):
536 """Set the list of email addresses that should be cc'd based on the changed
537 files in this CL.
538 """
539 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000540
541 def GetBranch(self):
542 """Returns the short branch name, e.g. 'master'."""
543 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000544 branchref = RunGit(['symbolic-ref', 'HEAD'],
545 stderr=subprocess2.VOID, error_ok=True).strip()
546 if not branchref:
547 return None
548 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549 self.branch = ShortBranchName(self.branchref)
550 return self.branch
551
552 def GetBranchRef(self):
553 """Returns the full branch name, e.g. 'refs/heads/master'."""
554 self.GetBranch() # Poke the lazy loader.
555 return self.branchref
556
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000557 @staticmethod
558 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000559 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000560 e.g. 'origin', 'refs/heads/master'
561 """
562 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000563 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
564 error_ok=True).strip()
565 if upstream_branch:
566 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
567 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000568 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
569 error_ok=True).strip()
570 if upstream_branch:
571 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000572 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000573 # Fall back on trying a git-svn upstream branch.
574 if settings.GetIsGitSvn():
575 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000576 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000577 # Else, try to guess the origin remote.
578 remote_branches = RunGit(['branch', '-r']).split()
579 if 'origin/master' in remote_branches:
580 # Fall back on origin/master if it exits.
581 remote = 'origin'
582 upstream_branch = 'refs/heads/master'
583 elif 'origin/trunk' in remote_branches:
584 # Fall back on origin/trunk if it exists. Generally a shared
585 # git-svn clone
586 remote = 'origin'
587 upstream_branch = 'refs/heads/trunk'
588 else:
589 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590Either pass complete "git diff"-style arguments, like
591 git cl upload origin/master
592or verify this branch is set up to track another (via the --track argument to
593"git checkout -b ...").""")
594
595 return remote, upstream_branch
596
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000597 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000598 return git_common.get_or_create_merge_base(self.GetBranch(),
599 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601 def GetUpstreamBranch(self):
602 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000603 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000605 upstream_branch = upstream_branch.replace('refs/heads/',
606 'refs/remotes/%s/' % remote)
607 upstream_branch = upstream_branch.replace('refs/branch-heads/',
608 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000609 self.upstream_branch = upstream_branch
610 return self.upstream_branch
611
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000612 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000613 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000614 remote, branch = None, self.GetBranch()
615 seen_branches = set()
616 while branch not in seen_branches:
617 seen_branches.add(branch)
618 remote, branch = self.FetchUpstreamTuple(branch)
619 branch = ShortBranchName(branch)
620 if remote != '.' or branch.startswith('refs/remotes'):
621 break
622 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000623 remotes = RunGit(['remote'], error_ok=True).split()
624 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000625 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000626 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000627 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000628 logging.warning('Could not determine which remote this change is '
629 'associated with, so defaulting to "%s". This may '
630 'not be what you want. You may prevent this message '
631 'by running "git svn info" as documented here: %s',
632 self._remote,
633 GIT_INSTRUCTIONS_URL)
634 else:
635 logging.warn('Could not determine which remote this change is '
636 'associated with. You may prevent this message by '
637 'running "git svn info" as documented here: %s',
638 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000639 branch = 'HEAD'
640 if branch.startswith('refs/remotes'):
641 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000642 elif branch.startswith('refs/branch-heads/'):
643 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000644 else:
645 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000646 return self._remote
647
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000648 def GitSanityChecks(self, upstream_git_obj):
649 """Checks git repo status and ensures diff is from local commits."""
650
sbc@chromium.org79706062015-01-14 21:18:12 +0000651 if upstream_git_obj is None:
652 if self.GetBranch() is None:
653 print >> sys.stderr, (
654 'ERROR: unable to dertermine current branch (detached HEAD?)')
655 else:
656 print >> sys.stderr, (
657 'ERROR: no upstream branch')
658 return False
659
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000660 # Verify the commit we're diffing against is in our current branch.
661 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
662 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
663 if upstream_sha != common_ancestor:
664 print >> sys.stderr, (
665 'ERROR: %s is not in the current branch. You may need to rebase '
666 'your tracking branch' % upstream_sha)
667 return False
668
669 # List the commits inside the diff, and verify they are all local.
670 commits_in_diff = RunGit(
671 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
672 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
673 remote_branch = remote_branch.strip()
674 if code != 0:
675 _, remote_branch = self.GetRemoteBranch()
676
677 commits_in_remote = RunGit(
678 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
679
680 common_commits = set(commits_in_diff) & set(commits_in_remote)
681 if common_commits:
682 print >> sys.stderr, (
683 'ERROR: Your diff contains %d commits already in %s.\n'
684 'Run "git log --oneline %s..HEAD" to get a list of commits in '
685 'the diff. If you are using a custom git flow, you can override'
686 ' the reference used for this check with "git config '
687 'gitcl.remotebranch <git-ref>".' % (
688 len(common_commits), remote_branch, upstream_git_obj))
689 return False
690 return True
691
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000692 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000693 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000694
695 Returns None if it is not set.
696 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000697 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
698 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000699
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000700 def GetGitSvnRemoteUrl(self):
701 """Return the configured git-svn remote URL parsed from git svn info.
702
703 Returns None if it is not set.
704 """
705 # URL is dependent on the current directory.
706 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
707 if data:
708 keys = dict(line.split(': ', 1) for line in data.splitlines()
709 if ': ' in line)
710 return keys.get('URL', None)
711 return None
712
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713 def GetRemoteUrl(self):
714 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
715
716 Returns None if there is no remote.
717 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000718 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000719 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
720
721 # If URL is pointing to a local directory, it is probably a git cache.
722 if os.path.isdir(url):
723 url = RunGit(['config', 'remote.%s.url' % remote],
724 error_ok=True,
725 cwd=url).strip()
726 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727
728 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000729 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000730 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000732 self.issue = int(issue) or None if issue else None
733 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.issue
735
736 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000737 if not self.rietveld_server:
738 # If we're on a branch then get the server potentially associated
739 # with that branch.
740 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000741 rietveld_server_config = self._RietveldServer()
742 if rietveld_server_config:
743 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
744 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000745 if not self.rietveld_server:
746 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 return self.rietveld_server
748
749 def GetIssueURL(self):
750 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000751 if not self.GetIssue():
752 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
754
755 def GetDescription(self, pretty=False):
756 if not self.has_description:
757 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000758 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000759 try:
760 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000761 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000762 if e.code == 404:
763 DieWithError(
764 ('\nWhile fetching the description for issue %d, received a '
765 '404 (not found)\n'
766 'error. It is likely that you deleted this '
767 'issue on the server. If this is the\n'
768 'case, please run\n\n'
769 ' git cl issue 0\n\n'
770 'to clear the association with the deleted issue. Then run '
771 'this command again.') % issue)
772 else:
773 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000774 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000775 except urllib2.URLError as e:
776 print >> sys.stderr, (
777 'Warning: Failed to retrieve CL description due to network '
778 'failure.')
779 self.description = ''
780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781 self.has_description = True
782 if pretty:
783 wrapper = textwrap.TextWrapper()
784 wrapper.initial_indent = wrapper.subsequent_indent = ' '
785 return wrapper.fill(self.description)
786 return self.description
787
788 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000789 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000790 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 patchset = RunGit(['config', self._PatchsetSetting()],
792 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000793 self.patchset = int(patchset) or None if patchset else None
794 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 return self.patchset
796
797 def SetPatchset(self, patchset):
798 """Set this branch's patchset. If patchset=0, clears the patchset."""
799 if patchset:
800 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000801 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 else:
803 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000804 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000805 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000807 def GetMostRecentPatchset(self):
808 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000809
810 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000811 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000812 '/download/issue%s_%s.diff' % (issue, patchset))
813
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000814 def GetIssueProperties(self):
815 if self._props is None:
816 issue = self.GetIssue()
817 if not issue:
818 self._props = {}
819 else:
820 self._props = self.RpcServer().get_issue_properties(issue, True)
821 return self._props
822
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000823 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000824 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000825
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000826 def AddComment(self, message):
827 return self.RpcServer().add_comment(self.GetIssue(), message)
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829 def SetIssue(self, issue):
830 """Set this branch's issue. If issue=0, clears the issue."""
831 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000832 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 RunGit(['config', self._IssueSetting(), str(issue)])
834 if self.rietveld_server:
835 RunGit(['config', self._RietveldServer(), self.rietveld_server])
836 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000837 current_issue = self.GetIssue()
838 if current_issue:
839 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000840 self.issue = None
841 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000843 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000844 if not self.GitSanityChecks(upstream_branch):
845 DieWithError('\nGit sanity check failure')
846
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000847 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000848 if not root:
849 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000850 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000851
852 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000853 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000854 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000855 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000856 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000857 except subprocess2.CalledProcessError:
858 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000859 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000860 'This branch probably doesn\'t exist anymore. To reset the\n'
861 'tracking branch, please run\n'
862 ' git branch --set-upstream %s trunk\n'
863 'replacing trunk with origin/master or the relevant branch') %
864 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000865
maruel@chromium.org52424302012-08-29 15:14:30 +0000866 issue = self.GetIssue()
867 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000868 if issue:
869 description = self.GetDescription()
870 else:
871 # If the change was never uploaded, use the log messages of all commits
872 # up to the branch point, as git cl upload will prefill the description
873 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000874 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
875 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000876
877 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000878 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000879 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000880 name,
881 description,
882 absroot,
883 files,
884 issue,
885 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000886 author,
887 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000888
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000889 def GetStatus(self):
890 """Apply a rough heuristic to give a simple summary of an issue's review
891 or CQ status, assuming adherence to a common workflow.
892
893 Returns None if no issue for this branch, or one of the following keywords:
894 * 'error' - error from review tool (including deleted issues)
895 * 'unsent' - not sent for review
896 * 'waiting' - waiting for review
897 * 'reply' - waiting for owner to reply to review
898 * 'lgtm' - LGTM from at least one approved reviewer
899 * 'commit' - in the commit queue
900 * 'closed' - closed
901 """
902 if not self.GetIssue():
903 return None
904
905 try:
906 props = self.GetIssueProperties()
907 except urllib2.HTTPError:
908 return 'error'
909
910 if props.get('closed'):
911 # Issue is closed.
912 return 'closed'
913 if props.get('commit'):
914 # Issue is in the commit queue.
915 return 'commit'
916
917 try:
918 reviewers = self.GetApprovingReviewers()
919 except urllib2.HTTPError:
920 return 'error'
921
922 if reviewers:
923 # Was LGTM'ed.
924 return 'lgtm'
925
926 messages = props.get('messages') or []
927
928 if not messages:
929 # No message was sent.
930 return 'unsent'
931 if messages[-1]['sender'] != props.get('owner_email'):
932 # Non-LGTM reply from non-owner
933 return 'reply'
934 return 'waiting'
935
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000936 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000937 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000938
939 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000940 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000941 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000942 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000943 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000944 except presubmit_support.PresubmitFailure, e:
945 DieWithError(
946 ('%s\nMaybe your depot_tools is out of date?\n'
947 'If all fails, contact maruel@') % e)
948
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000949 def UpdateDescription(self, description):
950 self.description = description
951 return self.RpcServer().update_description(
952 self.GetIssue(), self.description)
953
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000955 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000956 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000958 def SetFlag(self, flag, value):
959 """Patchset must match."""
960 if not self.GetPatchset():
961 DieWithError('The patchset needs to match. Send another patchset.')
962 try:
963 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000964 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000965 except urllib2.HTTPError, e:
966 if e.code == 404:
967 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
968 if e.code == 403:
969 DieWithError(
970 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
971 'match?') % (self.GetIssue(), self.GetPatchset()))
972 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000974 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975 """Returns an upload.RpcServer() to access this review's rietveld instance.
976 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000977 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000978 self._rpc_server = rietveld.CachingRietveld(
979 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000980 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981
982 def _IssueSetting(self):
983 """Return the git setting that stores this change's issue."""
984 return 'branch.%s.rietveldissue' % self.GetBranch()
985
986 def _PatchsetSetting(self):
987 """Return the git setting that stores this change's most recent patchset."""
988 return 'branch.%s.rietveldpatchset' % self.GetBranch()
989
990 def _RietveldServer(self):
991 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000992 branch = self.GetBranch()
993 if branch:
994 return 'branch.%s.rietveldserver' % branch
995 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996
997
998def GetCodereviewSettingsInteractively():
999 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001000 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001 server = settings.GetDefaultServerUrl(error_ok=True)
1002 prompt = 'Rietveld server (host[:port])'
1003 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001004 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 if not server and not newserver:
1006 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001007 if newserver:
1008 newserver = gclient_utils.UpgradeToHttps(newserver)
1009 if newserver != server:
1010 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001012 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 prompt = caption
1014 if initial:
1015 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001016 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 if new_val == 'x':
1018 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001019 elif new_val:
1020 if is_url:
1021 new_val = gclient_utils.UpgradeToHttps(new_val)
1022 if new_val != initial:
1023 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001025 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001026 SetProperty(settings.GetDefaultPrivateFlag(),
1027 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001029 'tree-status-url', False)
1030 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001031 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001032
1033 # TODO: configure a default branch to diff against, rather than this
1034 # svn-based hackery.
1035
1036
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001037class ChangeDescription(object):
1038 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001039 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001040 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001041
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001042 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001043 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001044
agable@chromium.org42c20792013-09-12 17:34:49 +00001045 @property # www.logilab.org/ticket/89786
1046 def description(self): # pylint: disable=E0202
1047 return '\n'.join(self._description_lines)
1048
1049 def set_description(self, desc):
1050 if isinstance(desc, basestring):
1051 lines = desc.splitlines()
1052 else:
1053 lines = [line.rstrip() for line in desc]
1054 while lines and not lines[0]:
1055 lines.pop(0)
1056 while lines and not lines[-1]:
1057 lines.pop(-1)
1058 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001059
piman@chromium.org336f9122014-09-04 02:16:55 +00001060 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001061 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001062 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001063 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001064 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001065 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001066
agable@chromium.org42c20792013-09-12 17:34:49 +00001067 # Get the set of R= and TBR= lines and remove them from the desciption.
1068 regexp = re.compile(self.R_LINE)
1069 matches = [regexp.match(line) for line in self._description_lines]
1070 new_desc = [l for i, l in enumerate(self._description_lines)
1071 if not matches[i]]
1072 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001073
agable@chromium.org42c20792013-09-12 17:34:49 +00001074 # Construct new unified R= and TBR= lines.
1075 r_names = []
1076 tbr_names = []
1077 for match in matches:
1078 if not match:
1079 continue
1080 people = cleanup_list([match.group(2).strip()])
1081 if match.group(1) == 'TBR':
1082 tbr_names.extend(people)
1083 else:
1084 r_names.extend(people)
1085 for name in r_names:
1086 if name not in reviewers:
1087 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001088 if add_owners_tbr:
1089 owners_db = owners.Database(change.RepositoryRoot(),
1090 fopen=file, os_path=os.path, glob=glob.glob)
1091 all_reviewers = set(tbr_names + reviewers)
1092 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1093 all_reviewers)
1094 tbr_names.extend(owners_db.reviewers_for(missing_files,
1095 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001096 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1097 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1098
1099 # Put the new lines in the description where the old first R= line was.
1100 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1101 if 0 <= line_loc < len(self._description_lines):
1102 if new_tbr_line:
1103 self._description_lines.insert(line_loc, new_tbr_line)
1104 if new_r_line:
1105 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001106 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001107 if new_r_line:
1108 self.append_footer(new_r_line)
1109 if new_tbr_line:
1110 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001111
1112 def prompt(self):
1113 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001114 self.set_description([
1115 '# Enter a description of the change.',
1116 '# This will be displayed on the codereview site.',
1117 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001118 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001119 '--------------------',
1120 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001121
agable@chromium.org42c20792013-09-12 17:34:49 +00001122 regexp = re.compile(self.BUG_LINE)
1123 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001124 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001125 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001126 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001127 if not content:
1128 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001129 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001130
1131 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001132 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1133 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001134 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001135 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001136
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001137 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001138 if self._description_lines:
1139 # Add an empty line if either the last line or the new line isn't a tag.
1140 last_line = self._description_lines[-1]
1141 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1142 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1143 self._description_lines.append('')
1144 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001145
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001146 def get_reviewers(self):
1147 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001148 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1149 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001150 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001151
1152
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001153def get_approving_reviewers(props):
1154 """Retrieves the reviewers that approved a CL from the issue properties with
1155 messages.
1156
1157 Note that the list may contain reviewers that are not committer, thus are not
1158 considered by the CQ.
1159 """
1160 return sorted(
1161 set(
1162 message['sender']
1163 for message in props['messages']
1164 if message['approval'] and message['sender'] in props['reviewers']
1165 )
1166 )
1167
1168
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169def FindCodereviewSettingsFile(filename='codereview.settings'):
1170 """Finds the given file starting in the cwd and going up.
1171
1172 Only looks up to the top of the repository unless an
1173 'inherit-review-settings-ok' file exists in the root of the repository.
1174 """
1175 inherit_ok_file = 'inherit-review-settings-ok'
1176 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001177 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1179 root = '/'
1180 while True:
1181 if filename in os.listdir(cwd):
1182 if os.path.isfile(os.path.join(cwd, filename)):
1183 return open(os.path.join(cwd, filename))
1184 if cwd == root:
1185 break
1186 cwd = os.path.dirname(cwd)
1187
1188
1189def LoadCodereviewSettingsFromFile(fileobj):
1190 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001191 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 def SetProperty(name, setting, unset_error_ok=False):
1194 fullname = 'rietveld.' + name
1195 if setting in keyvals:
1196 RunGit(['config', fullname, keyvals[setting]])
1197 else:
1198 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1199
1200 SetProperty('server', 'CODE_REVIEW_SERVER')
1201 # Only server setting is required. Other settings can be absent.
1202 # In that case, we ignore errors raised during option deletion attempt.
1203 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001204 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1206 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001207 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001208 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001209 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1210 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001211 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001212 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001213 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001215 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001216 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1219 #should be of the form
1220 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1221 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1222 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1223 keyvals['ORIGIN_URL_CONFIG']])
1224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001226def urlretrieve(source, destination):
1227 """urllib is broken for SSL connections via a proxy therefore we
1228 can't use urllib.urlretrieve()."""
1229 with open(destination, 'w') as f:
1230 f.write(urllib2.urlopen(source).read())
1231
1232
ukai@chromium.org712d6102013-11-27 00:52:58 +00001233def hasSheBang(fname):
1234 """Checks fname is a #! script."""
1235 with open(fname) as f:
1236 return f.read(2).startswith('#!')
1237
1238
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001239def DownloadHooks(force):
1240 """downloads hooks
1241
1242 Args:
1243 force: True to update hooks. False to install hooks if not present.
1244 """
1245 if not settings.GetIsGerrit():
1246 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001247 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001248 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1249 if not os.access(dst, os.X_OK):
1250 if os.path.exists(dst):
1251 if not force:
1252 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001253 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001254 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001255 if not hasSheBang(dst):
1256 DieWithError('Not a script: %s\n'
1257 'You need to download from\n%s\n'
1258 'into .git/hooks/commit-msg and '
1259 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001260 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1261 except Exception:
1262 if os.path.exists(dst):
1263 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001264 DieWithError('\nFailed to download hooks.\n'
1265 'You need to download from\n%s\n'
1266 'into .git/hooks/commit-msg and '
1267 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001268
1269
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001270@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001272 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001274 parser.add_option('--activate-update', action='store_true',
1275 help='activate auto-updating [rietveld] section in '
1276 '.git/config')
1277 parser.add_option('--deactivate-update', action='store_true',
1278 help='deactivate auto-updating [rietveld] section in '
1279 '.git/config')
1280 options, args = parser.parse_args(args)
1281
1282 if options.deactivate_update:
1283 RunGit(['config', 'rietveld.autoupdate', 'false'])
1284 return
1285
1286 if options.activate_update:
1287 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1288 return
1289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 if len(args) == 0:
1291 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001292 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 return 0
1294
1295 url = args[0]
1296 if not url.endswith('codereview.settings'):
1297 url = os.path.join(url, 'codereview.settings')
1298
1299 # Load code review settings and download hooks (if available).
1300 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001301 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 return 0
1303
1304
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001305def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001306 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001307 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1308 branch = ShortBranchName(branchref)
1309 _, args = parser.parse_args(args)
1310 if not args:
1311 print("Current base-url:")
1312 return RunGit(['config', 'branch.%s.base-url' % branch],
1313 error_ok=False).strip()
1314 else:
1315 print("Setting base-url to %s" % args[0])
1316 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1317 error_ok=False).strip()
1318
1319
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001320def color_for_status(status):
1321 """Maps a Changelist status to color, for CMDstatus and other tools."""
1322 return {
1323 'unsent': Fore.RED,
1324 'waiting': Fore.BLUE,
1325 'reply': Fore.YELLOW,
1326 'lgtm': Fore.GREEN,
1327 'commit': Fore.MAGENTA,
1328 'closed': Fore.CYAN,
1329 'error': Fore.WHITE,
1330 }.get(status, Fore.WHITE)
1331
1332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001334 """Show status of changelists.
1335
1336 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001337 - Red not sent for review or broken
1338 - Blue waiting for review
1339 - Yellow waiting for you to reply to review
1340 - Green LGTM'ed
1341 - Magenta in the commit queue
1342 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001343
1344 Also see 'git cl comments'.
1345 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346 parser.add_option('--field',
1347 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001348 parser.add_option('-f', '--fast', action='store_true',
1349 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001351 if args:
1352 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001355 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 if options.field.startswith('desc'):
1357 print cl.GetDescription()
1358 elif options.field == 'id':
1359 issueid = cl.GetIssue()
1360 if issueid:
1361 print issueid
1362 elif options.field == 'patch':
1363 patchset = cl.GetPatchset()
1364 if patchset:
1365 print patchset
1366 elif options.field == 'url':
1367 url = cl.GetIssueURL()
1368 if url:
1369 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001370 return 0
1371
1372 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1373 if not branches:
1374 print('No local branch found.')
1375 return 0
1376
1377 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001378 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001379 alignment = max(5, max(len(b) for b in branches))
1380 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001381 # Adhoc thread pool to request data concurrently.
1382 output = Queue.Queue()
1383
1384 # Silence upload.py otherwise it becomes unweldly.
1385 upload.verbosity = 0
1386
1387 if not options.fast:
1388 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001389 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001390 c = Changelist(branchref=b)
1391 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001392 status = c.GetStatus()
1393 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001394
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001395 if i and (not status or status == 'error'):
1396 # The issue probably doesn't exist anymore.
1397 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001398
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001399 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001401 # Process one branch synchronously to work through authentication, then
1402 # spawn threads to process all the other branches in parallel.
1403 if branches:
1404 fetch(branches[0])
1405 threads = [
1406 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001407 for t in threads:
1408 t.daemon = True
1409 t.start()
1410 else:
1411 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1412 for b in branches:
1413 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001414 url = c.GetIssueURL()
1415 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001416
1417 tmp = {}
1418 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001419 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001420 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001421 b, i, color = output.get()
1422 tmp[b] = (i, color)
1423 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001424 reset = Fore.RESET
1425 if not sys.stdout.isatty():
1426 color = ''
1427 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001428 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001429 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001430
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001431 cl = Changelist()
1432 print
1433 print 'Current branch:',
1434 if not cl.GetIssue():
1435 print 'no issue assigned.'
1436 return 0
1437 print cl.GetBranch()
1438 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001439 if not options.fast:
1440 print 'Issue description:'
1441 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442 return 0
1443
1444
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001445def colorize_CMDstatus_doc():
1446 """To be called once in main() to add colors to git cl status help."""
1447 colors = [i for i in dir(Fore) if i[0].isupper()]
1448
1449 def colorize_line(line):
1450 for color in colors:
1451 if color in line.upper():
1452 # Extract whitespaces first and the leading '-'.
1453 indent = len(line) - len(line.lstrip(' ')) + 1
1454 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1455 return line
1456
1457 lines = CMDstatus.__doc__.splitlines()
1458 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1459
1460
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001461@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001463 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464
1465 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001466 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001467 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
1469 cl = Changelist()
1470 if len(args) > 0:
1471 try:
1472 issue = int(args[0])
1473 except ValueError:
1474 DieWithError('Pass a number to set the issue or none to list it.\n'
1475 'Maybe you want to run git cl status?')
1476 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001477 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 return 0
1479
1480
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001481def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001482 """Shows or posts review comments for any changelist."""
1483 parser.add_option('-a', '--add-comment', dest='comment',
1484 help='comment to add to an issue')
1485 parser.add_option('-i', dest='issue',
1486 help="review issue id (defaults to current issue)")
1487 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001488
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001489 issue = None
1490 if options.issue:
1491 try:
1492 issue = int(options.issue)
1493 except ValueError:
1494 DieWithError('A review issue id is expected to be a number')
1495
1496 cl = Changelist(issue=issue)
1497
1498 if options.comment:
1499 cl.AddComment(options.comment)
1500 return 0
1501
1502 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001503 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001504 if message['disapproval']:
1505 color = Fore.RED
1506 elif message['approval']:
1507 color = Fore.GREEN
1508 elif message['sender'] == data['owner_email']:
1509 color = Fore.MAGENTA
1510 else:
1511 color = Fore.BLUE
1512 print '\n%s%s %s%s' % (
1513 color, message['date'].split('.', 1)[0], message['sender'],
1514 Fore.RESET)
1515 if message['text'].strip():
1516 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001517 return 0
1518
1519
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001520def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001521 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001522 cl = Changelist()
1523 if not cl.GetIssue():
1524 DieWithError('This branch has no associated changelist.')
1525 description = ChangeDescription(cl.GetDescription())
1526 description.prompt()
1527 cl.UpdateDescription(description.description)
1528 return 0
1529
1530
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531def CreateDescriptionFromLog(args):
1532 """Pulls out the commit log to use as a base for the CL description."""
1533 log_args = []
1534 if len(args) == 1 and not args[0].endswith('.'):
1535 log_args = [args[0] + '..']
1536 elif len(args) == 1 and args[0].endswith('...'):
1537 log_args = [args[0][:-1]]
1538 elif len(args) == 2:
1539 log_args = [args[0] + '..' + args[1]]
1540 else:
1541 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001542 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543
1544
thestig@chromium.org44202a22014-03-11 19:22:18 +00001545def CMDlint(parser, args):
1546 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001547 parser.add_option('--filter', action='append', metavar='-x,+y',
1548 help='Comma-separated list of cpplint\'s category-filters')
1549 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001550
1551 # Access to a protected member _XX of a client class
1552 # pylint: disable=W0212
1553 try:
1554 import cpplint
1555 import cpplint_chromium
1556 except ImportError:
1557 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1558 return 1
1559
1560 # Change the current working directory before calling lint so that it
1561 # shows the correct base.
1562 previous_cwd = os.getcwd()
1563 os.chdir(settings.GetRoot())
1564 try:
1565 cl = Changelist()
1566 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1567 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001568 if not files:
1569 print "Cannot lint an empty CL"
1570 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001571
1572 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001573 command = args + files
1574 if options.filter:
1575 command = ['--filter=' + ','.join(options.filter)] + command
1576 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001577
1578 white_regex = re.compile(settings.GetLintRegex())
1579 black_regex = re.compile(settings.GetLintIgnoreRegex())
1580 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1581 for filename in filenames:
1582 if white_regex.match(filename):
1583 if black_regex.match(filename):
1584 print "Ignoring file %s" % filename
1585 else:
1586 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1587 extra_check_functions)
1588 else:
1589 print "Skipping file %s" % filename
1590 finally:
1591 os.chdir(previous_cwd)
1592 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1593 if cpplint._cpplint_state.error_count != 0:
1594 return 1
1595 return 0
1596
1597
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001598def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001599 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001600 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001601 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001602 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001603 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 (options, args) = parser.parse_args(args)
1605
ukai@chromium.org259e4682012-10-25 07:36:33 +00001606 if not options.force and is_dirty_git_tree('presubmit'):
1607 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001608 return 1
1609
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001610 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611 if args:
1612 base_branch = args[0]
1613 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001614 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001615 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001617 cl.RunHook(
1618 committing=not options.upload,
1619 may_prompt=False,
1620 verbose=options.verbose,
1621 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001622 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623
1624
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001625def AddChangeIdToCommitMessage(options, args):
1626 """Re-commits using the current message, assumes the commit hook is in
1627 place.
1628 """
1629 log_desc = options.message or CreateDescriptionFromLog(args)
1630 git_command = ['commit', '--amend', '-m', log_desc]
1631 RunGit(git_command)
1632 new_log_desc = CreateDescriptionFromLog(args)
1633 if CHANGE_ID in new_log_desc:
1634 print 'git-cl: Added Change-Id to commit message.'
1635 else:
1636 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1637
1638
piman@chromium.org336f9122014-09-04 02:16:55 +00001639def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001640 """upload the current branch to gerrit."""
1641 # We assume the remote called "origin" is the one we want.
1642 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001643 gerrit_remote = 'origin'
ukai@chromium.orge8077812012-02-03 03:41:46 +00001644 branch = 'master'
1645 if options.target_branch:
1646 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001648 change_desc = ChangeDescription(
1649 options.message or CreateDescriptionFromLog(args))
1650 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001651 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001652 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001653
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001654 if options.squash:
1655 # Try to get the message from a previous upload.
1656 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1657 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1658 if not message:
1659 if not options.force:
1660 change_desc.prompt()
1661
1662 if CHANGE_ID not in change_desc.description:
1663 # Run the commit-msg hook without modifying the head commit by writing
1664 # the commit message to a temporary file and running the hook over it,
1665 # then reading the file back in.
1666 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1667 'commit-msg')
1668 file_handle, msg_file = tempfile.mkstemp(text=True,
1669 prefix='commit_msg')
1670 try:
1671 try:
1672 with os.fdopen(file_handle, 'w') as fileobj:
1673 fileobj.write(change_desc.description)
1674 finally:
1675 os.close(file_handle)
1676 RunCommand([commit_msg_hook, msg_file])
1677 change_desc.set_description(gclient_utils.FileRead(msg_file))
1678 finally:
1679 os.remove(msg_file)
1680
1681 if not change_desc.description:
1682 print "Description is empty; aborting."
1683 return 1
1684
1685 message = change_desc.description
1686
1687 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1688 if remote is '.':
1689 # If our upstream branch is local, we base our squashed commit on its
1690 # squashed version.
1691 parent = ('refs/heads/git_cl_uploads/' +
1692 scm.GIT.ShortBranchName(upstream_branch))
1693
1694 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1695 # will create additional CLs when uploading.
1696 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1697 RunGitSilent(['rev-parse', parent + ':'])):
1698 print 'Upload upstream branch ' + upstream_branch + ' first.'
1699 return 1
1700 else:
1701 parent = cl.GetCommonAncestorWithUpstream()
1702
1703 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1704 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1705 '-m', message]).strip()
1706 else:
1707 if CHANGE_ID not in change_desc.description:
1708 AddChangeIdToCommitMessage(options, args)
1709 ref_to_push = 'HEAD'
1710 parent = '%s/%s' % (gerrit_remote, branch)
1711
1712 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1713 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001714 if len(commits) > 1:
1715 print('WARNING: This will upload %d commits. Run the following command '
1716 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001717 print('git log %s..%s' % (parent, ref_to_push))
1718 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001719 'commit.')
1720 ask_for_data('About to upload; enter to confirm.')
1721
piman@chromium.org336f9122014-09-04 02:16:55 +00001722 if options.reviewers or options.tbr_owners:
1723 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724
ukai@chromium.orge8077812012-02-03 03:41:46 +00001725 receive_options = []
1726 cc = cl.GetCCList().split(',')
1727 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001728 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001729 cc = filter(None, cc)
1730 if cc:
1731 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001732 if change_desc.get_reviewers():
1733 receive_options.extend(
1734 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735
ukai@chromium.orge8077812012-02-03 03:41:46 +00001736 git_command = ['push']
1737 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001738 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001739 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001740 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001741 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001742
1743 if options.squash:
1744 head = RunGit(['rev-parse', 'HEAD']).strip()
1745 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1746
ukai@chromium.orge8077812012-02-03 03:41:46 +00001747 # TODO(ukai): parse Change-Id: and set issue number?
1748 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001749
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750
wittman@chromium.org455dc922015-01-26 20:15:50 +00001751def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1752 """Computes the remote branch ref to use for the CL.
1753
1754 Args:
1755 remote (str): The git remote for the CL.
1756 remote_branch (str): The git remote branch for the CL.
1757 target_branch (str): The target branch specified by the user.
1758 pending_prefix (str): The pending prefix from the settings.
1759 """
1760 if not (remote and remote_branch):
1761 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001762
wittman@chromium.org455dc922015-01-26 20:15:50 +00001763 if target_branch:
1764 # Cannonicalize branch references to the equivalent local full symbolic
1765 # refs, which are then translated into the remote full symbolic refs
1766 # below.
1767 if '/' not in target_branch:
1768 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1769 else:
1770 prefix_replacements = (
1771 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1772 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1773 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1774 )
1775 match = None
1776 for regex, replacement in prefix_replacements:
1777 match = re.search(regex, target_branch)
1778 if match:
1779 remote_branch = target_branch.replace(match.group(0), replacement)
1780 break
1781 if not match:
1782 # This is a branch path but not one we recognize; use as-is.
1783 remote_branch = target_branch
1784 elif (not remote_branch.startswith('refs/remotes/branch-heads') and
1785 not remote_branch.startswith('refs/remotes/%s/refs' % remote)):
1786 # Default to master for refs that are not branches.
1787 remote_branch = 'refs/remotes/%s/master' % remote
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001788
wittman@chromium.org455dc922015-01-26 20:15:50 +00001789 # Create the true path to the remote branch.
1790 # Does the following translation:
1791 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1792 # * refs/remotes/origin/master -> refs/heads/master
1793 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1794 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1795 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1796 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1797 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1798 'refs/heads/')
1799 elif remote_branch.startswith('refs/remotes/branch-heads'):
1800 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1801 # If a pending prefix exists then replace refs/ with it.
1802 if pending_prefix:
1803 remote_branch = remote_branch.replace('refs/', pending_prefix)
1804 return remote_branch
1805
1806
piman@chromium.org336f9122014-09-04 02:16:55 +00001807def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001808 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001809 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1810 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811 if options.emulate_svn_auto_props:
1812 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001813
1814 change_desc = None
1815
pgervais@chromium.org91141372014-01-09 23:27:20 +00001816 if options.email is not None:
1817 upload_args.extend(['--email', options.email])
1818
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001820 if options.title:
1821 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001822 if options.message:
1823 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001824 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001825 print ("This branch is associated with issue %s. "
1826 "Adding patch to that issue." % cl.GetIssue())
1827 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001828 if options.title:
1829 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001830 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001831 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001832 if options.reviewers or options.tbr_owners:
1833 change_desc.update_reviewers(options.reviewers,
1834 options.tbr_owners,
1835 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001836 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001837 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001838
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001839 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 print "Description is empty; aborting."
1841 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001842
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001843 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001844 if change_desc.get_reviewers():
1845 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001846 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001847 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001848 DieWithError("Must specify reviewers to send email.")
1849 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001850
1851 # We check this before applying rietveld.private assuming that in
1852 # rietveld.cc only addresses which we can send private CLs to are listed
1853 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1854 # --private is specified explicitly on the command line.
1855 if options.private:
1856 logging.warn('rietveld.cc is ignored since private flag is specified. '
1857 'You need to review and add them manually if necessary.')
1858 cc = cl.GetCCListWithoutDefault()
1859 else:
1860 cc = cl.GetCCList()
1861 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001862 if cc:
1863 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001865 if options.private or settings.GetDefaultPrivateFlag() == "True":
1866 upload_args.append('--private')
1867
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001868 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001869 if not options.find_copies:
1870 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872 # Include the upstream repo's URL in the change -- this is useful for
1873 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001874 remote_url = cl.GetGitBaseUrlFromConfig()
1875 if not remote_url:
1876 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001877 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001878 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001879 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1880 remote_url = (cl.GetRemoteUrl() + '@'
1881 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001882 if remote_url:
1883 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00001884 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00001885 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1886 settings.GetPendingRefPrefix())
1887 if target_ref:
1888 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001889
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001890 project = settings.GetProject()
1891 if project:
1892 upload_args.extend(['--project', project])
1893
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001895 upload_args = ['upload'] + upload_args + args
1896 logging.info('upload.RealMain(%s)', upload_args)
1897 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001898 issue = int(issue)
1899 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001900 except KeyboardInterrupt:
1901 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902 except:
1903 # If we got an exception after the user typed a description for their
1904 # change, back up the description before re-raising.
1905 if change_desc:
1906 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1907 print '\nGot exception while uploading -- saving description to %s\n' \
1908 % backup_path
1909 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001910 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911 backup_file.close()
1912 raise
1913
1914 if not cl.GetIssue():
1915 cl.SetIssue(issue)
1916 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001917
1918 if options.use_commit_queue:
1919 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920 return 0
1921
1922
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001923def cleanup_list(l):
1924 """Fixes a list so that comma separated items are put as individual items.
1925
1926 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1927 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1928 """
1929 items = sum((i.split(',') for i in l), [])
1930 stripped_items = (i.strip() for i in items)
1931 return sorted(filter(None, stripped_items))
1932
1933
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001934@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001935def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001936 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001937 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1938 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001939 parser.add_option('--bypass-watchlists', action='store_true',
1940 dest='bypass_watchlists',
1941 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001942 parser.add_option('-f', action='store_true', dest='force',
1943 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001944 parser.add_option('-m', dest='message', help='message for patchset')
1945 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001946 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001947 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001948 help='reviewer email addresses')
1949 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001950 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001951 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001952 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001953 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001954 parser.add_option('--emulate_svn_auto_props',
1955 '--emulate-svn-auto-props',
1956 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001957 dest="emulate_svn_auto_props",
1958 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001959 parser.add_option('-c', '--use-commit-queue', action='store_true',
1960 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001961 parser.add_option('--private', action='store_true',
1962 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001963 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001964 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00001965 metavar='TARGET',
1966 help='Apply CL to remote ref TARGET. ' +
1967 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001968 parser.add_option('--squash', action='store_true',
1969 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001970 parser.add_option('--email', default=None,
1971 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001972 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1973 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001974
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001975 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001976 (options, args) = parser.parse_args(args)
1977
ukai@chromium.org259e4682012-10-25 07:36:33 +00001978 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001979 return 1
1980
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001981 options.reviewers = cleanup_list(options.reviewers)
1982 options.cc = cleanup_list(options.cc)
1983
ukai@chromium.orge8077812012-02-03 03:41:46 +00001984 cl = Changelist()
1985 if args:
1986 # TODO(ukai): is it ok for gerrit case?
1987 base_branch = args[0]
1988 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00001989 if cl.GetBranch() is None:
1990 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1991
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001992 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001993 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001994 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001995
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001996 # Apply watchlists on upload.
1997 change = cl.GetChange(base_branch, None)
1998 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1999 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002000 if not options.bypass_watchlists:
2001 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002002
ukai@chromium.orge8077812012-02-03 03:41:46 +00002003 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002004 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002005 # Set the reviewer list now so that presubmit checks can access it.
2006 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002007 change_description.update_reviewers(options.reviewers,
2008 options.tbr_owners,
2009 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002010 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002011 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002012 may_prompt=not options.force,
2013 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002014 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002015 if not hook_results.should_continue():
2016 return 1
2017 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002018 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002019
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002020 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002021 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002022 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002023 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002024 print ('The last upload made from this repository was patchset #%d but '
2025 'the most recent patchset on the server is #%d.'
2026 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002027 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2028 'from another machine or branch the patch you\'re uploading now '
2029 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002030 ask_for_data('About to upload; enter to confirm.')
2031
iannucci@chromium.org79540052012-10-19 23:15:26 +00002032 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002033 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002034 return GerritUpload(options, args, cl, change)
2035 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002036 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002037 git_set_branch_value('last-upload-hash',
2038 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002039
2040 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002041
2042
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002043def IsSubmoduleMergeCommit(ref):
2044 # When submodules are added to the repo, we expect there to be a single
2045 # non-git-svn merge commit at remote HEAD with a signature comment.
2046 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002047 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002048 return RunGit(cmd) != ''
2049
2050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002051def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002052 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002054 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055 Updates changelog with metadata (e.g. pointer to review).
2056 Pushes/dcommits the code upstream.
2057 Updates review and closes.
2058 """
2059 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2060 help='bypass upload presubmit hook')
2061 parser.add_option('-m', dest='message',
2062 help="override review description")
2063 parser.add_option('-f', action='store_true', dest='force',
2064 help="force yes to questions (don't prompt)")
2065 parser.add_option('-c', dest='contributor',
2066 help="external contributor for patch (appended to " +
2067 "description and used as author for git). Should be " +
2068 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002069 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070 (options, args) = parser.parse_args(args)
2071 cl = Changelist()
2072
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002073 current = cl.GetBranch()
2074 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2075 if not settings.GetIsGitSvn() and remote == '.':
2076 print
2077 print 'Attempting to push branch %r into another local branch!' % current
2078 print
2079 print 'Either reparent this branch on top of origin/master:'
2080 print ' git reparent-branch --root'
2081 print
2082 print 'OR run `git rebase-update` if you think the parent branch is already'
2083 print 'committed.'
2084 print
2085 print ' Current parent: %r' % upstream_branch
2086 return 1
2087
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002088 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089 # Default to merging against our best guess of the upstream branch.
2090 args = [cl.GetUpstreamBranch()]
2091
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002092 if options.contributor:
2093 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2094 print "Please provide contibutor as 'First Last <email@example.com>'"
2095 return 1
2096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002097 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002098 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099
ukai@chromium.org259e4682012-10-25 07:36:33 +00002100 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101 return 1
2102
2103 # This rev-list syntax means "show all commits not in my branch that
2104 # are in base_branch".
2105 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2106 base_branch]).splitlines()
2107 if upstream_commits:
2108 print ('Base branch "%s" has %d commits '
2109 'not in this branch.' % (base_branch, len(upstream_commits)))
2110 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2111 return 1
2112
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002113 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002114 svn_head = None
2115 if cmd == 'dcommit' or base_has_submodules:
2116 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2117 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002120 # If the base_head is a submodule merge commit, the first parent of the
2121 # base_head should be a git-svn commit, which is what we're interested in.
2122 base_svn_head = base_branch
2123 if base_has_submodules:
2124 base_svn_head += '^1'
2125
2126 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 if extra_commits:
2128 print ('This branch has %d additional commits not upstreamed yet.'
2129 % len(extra_commits.splitlines()))
2130 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2131 'before attempting to %s.' % (base_branch, cmd))
2132 return 1
2133
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002134 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002135 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002136 author = None
2137 if options.contributor:
2138 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002139 hook_results = cl.RunHook(
2140 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002141 may_prompt=not options.force,
2142 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002143 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002144 if not hook_results.should_continue():
2145 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002146
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002147 # Check the tree status if the tree status URL is set.
2148 status = GetTreeStatus()
2149 if 'closed' == status:
2150 print('The tree is closed. Please wait for it to reopen. Use '
2151 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2152 return 1
2153 elif 'unknown' == status:
2154 print('Unable to determine tree status. Please verify manually and '
2155 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2156 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002157 else:
2158 breakpad.SendStack(
2159 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002160 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2161 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002162 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002163
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002164 change_desc = ChangeDescription(options.message)
2165 if not change_desc.description and cl.GetIssue():
2166 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002167
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002168 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002169 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002170 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002171 else:
2172 print 'No description set.'
2173 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2174 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002175
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002176 # Keep a separate copy for the commit message, because the commit message
2177 # contains the link to the Rietveld issue, while the Rietveld message contains
2178 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002179 # Keep a separate copy for the commit message.
2180 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002181 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002182
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002183 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002184 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002185 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002186 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002187 commit_desc.append_footer('Patch from %s.' % options.contributor)
2188
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002189 print('Description:')
2190 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002191
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002192 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002193 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002194 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002196 # We want to squash all this branch's commits into one commit with the proper
2197 # description. We do this by doing a "reset --soft" to the base branch (which
2198 # keeps the working copy the same), then dcommitting that. If origin/master
2199 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2200 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002202 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2203 # Delete the branches if they exist.
2204 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2205 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2206 result = RunGitWithCode(showref_cmd)
2207 if result[0] == 0:
2208 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002209
2210 # We might be in a directory that's present in this branch but not in the
2211 # trunk. Move up to the top of the tree so that git commands that expect a
2212 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002213 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002214 if rel_base_path:
2215 os.chdir(rel_base_path)
2216
2217 # Stuff our change into the merge branch.
2218 # We wrap in a try...finally block so if anything goes wrong,
2219 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002220 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002221 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002222 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002223 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002224 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002225 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002226 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002227 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002228 RunGit(
2229 [
2230 'commit', '--author', options.contributor,
2231 '-m', commit_desc.description,
2232 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002233 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002234 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002235 if base_has_submodules:
2236 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2237 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2238 RunGit(['checkout', CHERRY_PICK_BRANCH])
2239 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002240 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002241 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002242 pending_prefix = settings.GetPendingRefPrefix()
2243 if not pending_prefix or branch.startswith(pending_prefix):
2244 # If not using refs/pending/heads/* at all, or target ref is already set
2245 # to pending, then push to the target ref directly.
2246 retcode, output = RunGitWithCode(
2247 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002248 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002249 else:
2250 # Cherry-pick the change on top of pending ref and then push it.
2251 assert branch.startswith('refs/'), branch
2252 assert pending_prefix[-1] == '/', pending_prefix
2253 pending_ref = pending_prefix + branch[len('refs/'):]
2254 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002255 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002256 if retcode == 0:
2257 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002258 else:
2259 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002260 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002261 'svn', 'dcommit',
2262 '-C%s' % options.similarity,
2263 '--no-rebase', '--rmdir',
2264 ]
2265 if settings.GetForceHttpsCommitUrl():
2266 # Allow forcing https commit URLs for some projects that don't allow
2267 # committing to http URLs (like Google Code).
2268 remote_url = cl.GetGitSvnRemoteUrl()
2269 if urlparse.urlparse(remote_url).scheme == 'http':
2270 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002271 cmd_args.append('--commit-url=%s' % remote_url)
2272 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002273 if 'Committed r' in output:
2274 revision = re.match(
2275 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2276 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002277 finally:
2278 # And then swap back to the original branch and clean up.
2279 RunGit(['checkout', '-q', cl.GetBranch()])
2280 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002281 if base_has_submodules:
2282 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002283
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002284 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002285 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002286 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002287
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002288 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002289 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002290 try:
2291 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2292 # We set pushed_to_pending to False, since it made it all the way to the
2293 # real ref.
2294 pushed_to_pending = False
2295 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002296 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002297
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002298 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002299 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002300 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002301 if not to_pending:
2302 if viewvc_url and revision:
2303 change_desc.append_footer(
2304 'Committed: %s%s' % (viewvc_url, revision))
2305 elif revision:
2306 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002307 print ('Closing issue '
2308 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002309 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002310 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002311 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002312 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002313 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002314 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002315 if options.bypass_hooks:
2316 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2317 else:
2318 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002319 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002320 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002321
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002322 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002323 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2324 print 'The commit is in the pending queue (%s).' % pending_ref
2325 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002326 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002327 'footer.' % branch)
2328
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002329 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2330 if os.path.isfile(hook):
2331 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002332
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002333 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334
2335
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002336def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2337 print
2338 print 'Waiting for commit to be landed on %s...' % real_ref
2339 print '(If you are impatient, you may Ctrl-C once without harm)'
2340 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2341 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2342
2343 loop = 0
2344 while True:
2345 sys.stdout.write('fetching (%d)... \r' % loop)
2346 sys.stdout.flush()
2347 loop += 1
2348
2349 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2350 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2351 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2352 for commit in commits.splitlines():
2353 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2354 print 'Found commit on %s' % real_ref
2355 return commit
2356
2357 current_rev = to_rev
2358
2359
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002360def PushToGitPending(remote, pending_ref, upstream_ref):
2361 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2362
2363 Returns:
2364 (retcode of last operation, output log of last operation).
2365 """
2366 assert pending_ref.startswith('refs/'), pending_ref
2367 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2368 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2369 code = 0
2370 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002371 max_attempts = 3
2372 attempts_left = max_attempts
2373 while attempts_left:
2374 if attempts_left != max_attempts:
2375 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2376 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002377
2378 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002379 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002380 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002381 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002382 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002383 print 'Fetch failed with exit code %d.' % code
2384 if out.strip():
2385 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002386 continue
2387
2388 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002389 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002390 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002391 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002392 if code:
2393 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002394 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2395 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002396 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2397 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002398 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002399 return code, out
2400
2401 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002402 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002403 code, out = RunGitWithCode(
2404 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2405 if code == 0:
2406 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002407 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002408 return code, out
2409
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002410 print 'Push failed with exit code %d.' % code
2411 if out.strip():
2412 print out.strip()
2413 if IsFatalPushFailure(out):
2414 print (
2415 'Fatal push error. Make sure your .netrc credentials and git '
2416 'user.email are correct and you have push access to the repo.')
2417 return code, out
2418
2419 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002420 return code, out
2421
2422
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002423def IsFatalPushFailure(push_stdout):
2424 """True if retrying push won't help."""
2425 return '(prohibited by Gerrit)' in push_stdout
2426
2427
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002428@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002429def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002430 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002431 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002432 message = """This doesn't appear to be an SVN repository.
2433If your project has a git mirror with an upstream SVN master, you probably need
2434to run 'git svn init', see your project's git mirror documentation.
2435If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002436to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002437Choose wisely, if you get this wrong, your commit might appear to succeed but
2438will instead be silently ignored."""
2439 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002440 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002441 return SendUpstream(parser, args, 'dcommit')
2442
2443
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002444@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002445def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002446 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002447 if settings.GetIsGitSvn():
2448 print('This appears to be an SVN repository.')
2449 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002450 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002451 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002452
2453
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002454@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002455def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002456 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002457 parser.add_option('-b', dest='newbranch',
2458 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002459 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002460 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002461 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2462 help='Change to the directory DIR immediately, '
2463 'before doing anything else.')
2464 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002465 help='failed patches spew .rej files rather than '
2466 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002467 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2468 help="don't commit after patch applies")
2469 (options, args) = parser.parse_args(args)
2470 if len(args) != 1:
2471 parser.print_help()
2472 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002473 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002475 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002476 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002477
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002478 if options.newbranch:
2479 if options.force:
2480 RunGit(['branch', '-D', options.newbranch],
2481 stderr=subprocess2.PIPE, error_ok=True)
2482 RunGit(['checkout', '-b', options.newbranch,
2483 Changelist().GetUpstreamBranch()])
2484
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002485 return PatchIssue(issue_arg, options.reject, options.nocommit,
2486 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002487
2488
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002489def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002490 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002491 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002492 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002493 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002494 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002495 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002496 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002497 # Assume it's a URL to the patch. Default to https.
2498 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002499 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002500 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002501 DieWithError('Must pass an issue ID or full URL for '
2502 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002503 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002504 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002505 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507 # Switch up to the top-level directory, if necessary, in preparation for
2508 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002509 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002510 if top:
2511 os.chdir(top)
2512
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002513 # Git patches have a/ at the beginning of source paths. We strip that out
2514 # with a sed script rather than the -p flag to patch so we can feed either
2515 # Git or svn-style patches into the same apply command.
2516 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002517 try:
2518 patch_data = subprocess2.check_output(
2519 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2520 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002521 DieWithError('Git patch mungling failed.')
2522 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002524 # We use "git apply" to apply the patch instead of "patch" so that we can
2525 # pick up file adds.
2526 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002527 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002528 if directory:
2529 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002530 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002531 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002532 elif IsGitVersionAtLeast('1.7.12'):
2533 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002534 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002535 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002536 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002537 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002538 DieWithError('Failed to apply the patch')
2539
2540 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002541 if not nocommit:
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002542 RunGit(['commit', '-m', ('patch from issue %(i)s at patchset '
2543 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2544 % {'i': issue, 'p': patchset})])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002545 cl = Changelist()
2546 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002547 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002548 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002549 else:
2550 print "Patch applied to index."
2551 return 0
2552
2553
2554def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002555 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002556 # Provide a wrapper for git svn rebase to help avoid accidental
2557 # git svn dcommit.
2558 # It's the only command that doesn't use parser at all since we just defer
2559 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002560
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002561 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002562
2563
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002564def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002565 """Fetches the tree status and returns either 'open', 'closed',
2566 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002567 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002568 if url:
2569 status = urllib2.urlopen(url).read().lower()
2570 if status.find('closed') != -1 or status == '0':
2571 return 'closed'
2572 elif status.find('open') != -1 or status == '1':
2573 return 'open'
2574 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575 return 'unset'
2576
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002577
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002578def GetTreeStatusReason():
2579 """Fetches the tree status from a json url and returns the message
2580 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002581 url = settings.GetTreeStatusUrl()
2582 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002583 connection = urllib2.urlopen(json_url)
2584 status = json.loads(connection.read())
2585 connection.close()
2586 return status['message']
2587
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002588
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002589def GetBuilderMaster(bot_list):
2590 """For a given builder, fetch the master from AE if available."""
2591 map_url = 'https://builders-map.appspot.com/'
2592 try:
2593 master_map = json.load(urllib2.urlopen(map_url))
2594 except urllib2.URLError as e:
2595 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2596 (map_url, e))
2597 except ValueError as e:
2598 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2599 if not master_map:
2600 return None, 'Failed to build master map.'
2601
2602 result_master = ''
2603 for bot in bot_list:
2604 builder = bot.split(':', 1)[0]
2605 master_list = master_map.get(builder, [])
2606 if not master_list:
2607 return None, ('No matching master for builder %s.' % builder)
2608 elif len(master_list) > 1:
2609 return None, ('The builder name %s exists in multiple masters %s.' %
2610 (builder, master_list))
2611 else:
2612 cur_master = master_list[0]
2613 if not result_master:
2614 result_master = cur_master
2615 elif result_master != cur_master:
2616 return None, 'The builders do not belong to the same master.'
2617 return result_master, None
2618
2619
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002620def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002621 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002622 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002623 status = GetTreeStatus()
2624 if 'unset' == status:
2625 print 'You must configure your tree status URL by running "git cl config".'
2626 return 2
2627
2628 print "The tree is %s" % status
2629 print
2630 print GetTreeStatusReason()
2631 if status != 'open':
2632 return 1
2633 return 0
2634
2635
maruel@chromium.org15192402012-09-06 12:38:29 +00002636def CMDtry(parser, args):
2637 """Triggers a try job through Rietveld."""
2638 group = optparse.OptionGroup(parser, "Try job options")
2639 group.add_option(
2640 "-b", "--bot", action="append",
2641 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2642 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002643 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002644 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002645 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002646 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002647 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002648 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002649 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002650 "-r", "--revision",
2651 help="Revision to use for the try job; default: the "
2652 "revision will be determined by the try server; see "
2653 "its waterfall for more info")
2654 group.add_option(
2655 "-c", "--clobber", action="store_true", default=False,
2656 help="Force a clobber before building; e.g. don't do an "
2657 "incremental build")
2658 group.add_option(
2659 "--project",
2660 help="Override which project to use. Projects are defined "
2661 "server-side to define what default bot set to use")
2662 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002663 "-n", "--name", help="Try job name; default to current branch name")
2664 parser.add_option_group(group)
2665 options, args = parser.parse_args(args)
2666
2667 if args:
2668 parser.error('Unknown arguments: %s' % args)
2669
2670 cl = Changelist()
2671 if not cl.GetIssue():
2672 parser.error('Need to upload first')
2673
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002674 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002675 if props.get('closed'):
2676 parser.error('Cannot send tryjobs for a closed CL')
2677
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002678 if props.get('private'):
2679 parser.error('Cannot use trybots with private issue')
2680
maruel@chromium.org15192402012-09-06 12:38:29 +00002681 if not options.name:
2682 options.name = cl.GetBranch()
2683
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002684 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002685 options.master, err_msg = GetBuilderMaster(options.bot)
2686 if err_msg:
2687 parser.error('Tryserver master cannot be found because: %s\n'
2688 'Please manually specify the tryserver master'
2689 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002690
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002691 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002692 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002693 if not options.bot:
2694 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002695
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002696 # Get try masters from PRESUBMIT.py files.
2697 masters = presubmit_support.DoGetTryMasters(
2698 change,
2699 change.LocalPaths(),
2700 settings.GetRoot(),
2701 None,
2702 None,
2703 options.verbose,
2704 sys.stdout)
2705 if masters:
2706 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002707
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002708 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2709 options.bot = presubmit_support.DoGetTrySlaves(
2710 change,
2711 change.LocalPaths(),
2712 settings.GetRoot(),
2713 None,
2714 None,
2715 options.verbose,
2716 sys.stdout)
2717 if not options.bot:
2718 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002719
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002720 builders_and_tests = {}
2721 # TODO(machenbach): The old style command-line options don't support
2722 # multiple try masters yet.
2723 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2724 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2725
2726 for bot in old_style:
2727 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002728 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002729 elif ',' in bot:
2730 parser.error('Specify one bot per --bot flag')
2731 else:
2732 builders_and_tests.setdefault(bot, []).append('defaulttests')
2733
2734 for bot, tests in new_style:
2735 builders_and_tests.setdefault(bot, []).extend(tests)
2736
2737 # Return a master map with one master to be backwards compatible. The
2738 # master name defaults to an empty string, which will cause the master
2739 # not to be set on rietveld (deprecated).
2740 return {options.master: builders_and_tests}
2741
2742 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002743
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002744 for builders in masters.itervalues():
2745 if any('triggered' in b for b in builders):
2746 print >> sys.stderr, (
2747 'ERROR You are trying to send a job to a triggered bot. This type of'
2748 ' bot requires an\ninitial job from a parent (usually a builder). '
2749 'Instead send your job to the parent.\n'
2750 'Bot list: %s' % builders)
2751 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002752
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002753 patchset = cl.GetMostRecentPatchset()
2754 if patchset and patchset != cl.GetPatchset():
2755 print(
2756 '\nWARNING Mismatch between local config and server. Did a previous '
2757 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2758 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002759 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002760 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002761 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002762 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002763 except urllib2.HTTPError, e:
2764 if e.code == 404:
2765 print('404 from rietveld; '
2766 'did you mean to use "git try" instead of "git cl try"?')
2767 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002768 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002769
2770 for (master, builders) in masters.iteritems():
2771 if master:
2772 print 'Master: %s' % master
2773 length = max(len(builder) for builder in builders)
2774 for builder in sorted(builders):
2775 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002776 return 0
2777
2778
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002779@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002780def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002781 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002782 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002783 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002784 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002786 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002787 if args:
2788 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002789 branch = cl.GetBranch()
2790 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002791 cl = Changelist()
2792 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002793
2794 # Clear configured merge-base, if there is one.
2795 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002796 else:
2797 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002798 return 0
2799
2800
thestig@chromium.org00858c82013-12-02 23:08:03 +00002801def CMDweb(parser, args):
2802 """Opens the current CL in the web browser."""
2803 _, args = parser.parse_args(args)
2804 if args:
2805 parser.error('Unrecognized args: %s' % ' '.join(args))
2806
2807 issue_url = Changelist().GetIssueURL()
2808 if not issue_url:
2809 print >> sys.stderr, 'ERROR No issue to open'
2810 return 1
2811
2812 webbrowser.open(issue_url)
2813 return 0
2814
2815
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002816def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002817 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002818 _, args = parser.parse_args(args)
2819 if args:
2820 parser.error('Unrecognized args: %s' % ' '.join(args))
2821 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002822 props = cl.GetIssueProperties()
2823 if props.get('private'):
2824 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002825 cl.SetFlag('commit', '1')
2826 return 0
2827
2828
groby@chromium.org411034a2013-02-26 15:12:01 +00002829def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002830 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002831 _, args = parser.parse_args(args)
2832 if args:
2833 parser.error('Unrecognized args: %s' % ' '.join(args))
2834 cl = Changelist()
2835 # Ensure there actually is an issue to close.
2836 cl.GetDescription()
2837 cl.CloseIssue()
2838 return 0
2839
2840
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002841def CMDdiff(parser, args):
2842 """shows differences between local tree and last upload."""
2843 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002844 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002845 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002846 if not issue:
2847 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002848 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002849 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002850
2851 # Create a new branch based on the merge-base
2852 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2853 try:
2854 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002855 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002856 if rtn != 0:
2857 return rtn
2858
wychen@chromium.org06928532015-02-03 02:11:29 +00002859 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002860 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00002861 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002862 finally:
2863 RunGit(['checkout', '-q', branch])
2864 RunGit(['branch', '-D', TMP_BRANCH])
2865
2866 return 0
2867
2868
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002869def CMDowners(parser, args):
2870 """interactively find the owners for reviewing"""
2871 parser.add_option(
2872 '--no-color',
2873 action='store_true',
2874 help='Use this option to disable color output')
2875 options, args = parser.parse_args(args)
2876
2877 author = RunGit(['config', 'user.email']).strip() or None
2878
2879 cl = Changelist()
2880
2881 if args:
2882 if len(args) > 1:
2883 parser.error('Unknown args')
2884 base_branch = args[0]
2885 else:
2886 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002887 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002888
2889 change = cl.GetChange(base_branch, None)
2890 return owners_finder.OwnersFinder(
2891 [f.LocalPath() for f in
2892 cl.GetChange(base_branch, None).AffectedFiles()],
2893 change.RepositoryRoot(), author,
2894 fopen=file, os_path=os.path, glob=glob.glob,
2895 disable_color=options.no_color).run()
2896
2897
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002898def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
2899 """Generates a diff command."""
2900 # Generate diff for the current branch's changes.
2901 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
2902 upstream_commit, '--' ]
2903
2904 if args:
2905 for arg in args:
2906 if os.path.isdir(arg):
2907 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
2908 elif os.path.isfile(arg):
2909 diff_cmd.append(arg)
2910 else:
2911 DieWithError('Argument "%s" is not a file or a directory' % arg)
2912 else:
2913 diff_cmd.extend('*' + ext for ext in extensions)
2914
2915 return diff_cmd
2916
2917
enne@chromium.org555cfe42014-01-29 18:21:39 +00002918@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002919def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002920 """Runs clang-format on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00002921 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002922 parser.add_option('--full', action='store_true',
2923 help='Reformat the full content of all touched files')
2924 parser.add_option('--dry-run', action='store_true',
2925 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002926 parser.add_option('--diff', action='store_true',
2927 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002928 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002929
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002930 # git diff generates paths against the root of the repository. Change
2931 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002932 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002933 if rel_base_path:
2934 os.chdir(rel_base_path)
2935
digit@chromium.org29e47272013-05-17 17:01:46 +00002936 # Grab the merge-base commit, i.e. the upstream commit of the current
2937 # branch when it was created or the last time it was rebased. This is
2938 # to cover the case where the user may have called "git fetch origin",
2939 # moving the origin branch to a newer commit, but hasn't rebased yet.
2940 upstream_commit = None
2941 cl = Changelist()
2942 upstream_branch = cl.GetUpstreamBranch()
2943 if upstream_branch:
2944 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2945 upstream_commit = upstream_commit.strip()
2946
2947 if not upstream_commit:
2948 DieWithError('Could not find base commit for this branch. '
2949 'Are you in detached state?')
2950
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002951 if opts.full:
2952 # Only list the names of modified files.
2953 clang_diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00002954 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002955 # Only generate context-less patches.
2956 clang_diff_type = '-U0'
2957
2958 diff_cmd = BuildGitDiffCmd(clang_diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00002959 diff_output = RunGit(diff_cmd)
2960
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002961 top_dir = os.path.normpath(
2962 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2963
2964 # Locate the clang-format binary in the checkout
2965 try:
2966 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2967 except clang_format.NotFoundError, e:
2968 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002969
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002970 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
2971 # formatted. This is used to block during the presubmit.
2972 return_value = 0
2973
digit@chromium.org29e47272013-05-17 17:01:46 +00002974 if opts.full:
2975 # diff_output is a list of files to send to clang-format.
2976 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00002977 if files:
2978 cmd = [clang_format_tool]
2979 if not opts.dry_run and not opts.diff:
2980 cmd.append('-i')
2981 stdout = RunCommand(cmd + files, cwd=top_dir)
2982 if opts.diff:
2983 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002984 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002985 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00002986 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00002987 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002988 try:
2989 script = clang_format.FindClangFormatScriptInChromiumTree(
2990 'clang-format-diff.py')
2991 except clang_format.NotFoundError, e:
2992 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002993
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002994 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002995 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002996 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002997
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002998 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002999 if opts.diff:
3000 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003001 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003002 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003003
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003004 # Build a diff command that only operates on dart files. dart's formatter
3005 # does not have the nice property of only operating on modified chunks, so
3006 # hard code full.
3007 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3008 args, ['.dart'])
3009 dart_diff_output = RunGit(dart_diff_cmd)
3010 if dart_diff_output:
3011 try:
3012 command = [dart_format.FindDartFmtToolInChromiumTree()]
3013 if not opts.dry_run and not opts.diff:
3014 command.append('-w')
3015 command.extend(dart_diff_output.splitlines())
3016
3017 stdout = RunCommand(command, cwd=top_dir, env=env)
3018 if opts.dry_run and stdout:
3019 return_value = 2
3020 except dart_format.NotFoundError as e:
3021 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3022 'this checkout.')
3023
3024 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003025
3026
maruel@chromium.org29404b52014-09-08 22:58:00 +00003027def CMDlol(parser, args):
3028 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003029 print zlib.decompress(base64.b64decode(
3030 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3031 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3032 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3033 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003034 return 0
3035
3036
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003037class OptionParser(optparse.OptionParser):
3038 """Creates the option parse and add --verbose support."""
3039 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003040 optparse.OptionParser.__init__(
3041 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003042 self.add_option(
3043 '-v', '--verbose', action='count', default=0,
3044 help='Use 2 times for more debugging info')
3045
3046 def parse_args(self, args=None, values=None):
3047 options, args = optparse.OptionParser.parse_args(self, args, values)
3048 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3049 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3050 return options, args
3051
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003053def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003054 if sys.hexversion < 0x02060000:
3055 print >> sys.stderr, (
3056 '\nYour python version %s is unsupported, please upgrade.\n' %
3057 sys.version.split(' ', 1)[0])
3058 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003059
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003060 # Reload settings.
3061 global settings
3062 settings = Settings()
3063
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003064 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003065 dispatcher = subcommand.CommandDispatcher(__name__)
3066 try:
3067 return dispatcher.execute(OptionParser(), argv)
3068 except urllib2.HTTPError, e:
3069 if e.code != 500:
3070 raise
3071 DieWithError(
3072 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3073 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003074
3075
3076if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003077 # These affect sys.stdout so do it outside of main() to simplify mocks in
3078 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003079 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003080 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003081 sys.exit(main(sys.argv[1:]))