blob: e8695e82eff5e402b5d7ee09ecaa23216ea75c6b [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000010import datetime
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000013import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import logging
15import optparse
16import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000017import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import textwrap
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000022import time
maruel@chromium.org1033efd2013-07-23 23:25:09 +000023import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000025import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000026import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027
28try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000029 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030except ImportError:
31 pass
32
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035from third_party import upload
36import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
41import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000043import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000046import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047import watchlists
48
maruel@chromium.org0633fb42013-08-16 20:06:14 +000049__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000051DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000052POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000054GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000055CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056
thestig@chromium.org44202a22014-03-11 19:22:18 +000057# Valid extensions for files we want to lint.
58DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
59DEFAULT_LINT_IGNORE_REGEX = r"$^"
60
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000061# Shortcut since it quickly becomes redundant.
62Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000063
maruel@chromium.orgddd59412011-11-30 14:20:38 +000064# Initialized in main()
65settings = None
66
67
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000069 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070 sys.exit(1)
71
72
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000073def GetNoGitPagerEnv():
74 env = os.environ.copy()
75 # 'cat' is a magical git string that disables pagers on all platforms.
76 env['GIT_PAGER'] = 'cat'
77 return env
78
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000081 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000082 except subprocess2.CalledProcessError as e:
83 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000086 'Command "%s" failed.\n%s' % (
87 ' '.join(args), error_message or e.stdout or ''))
88 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089
90
91def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000092 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000093 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000094
95
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000096def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000098 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000099 if suppress_stderr:
100 stderr = subprocess2.VOID
101 else:
102 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000103 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000104 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000105 stdout=subprocess2.PIPE,
106 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000107 return code, out[0]
108 except ValueError:
109 # When the subprocess fails, it returns None. That triggers a ValueError
110 # when trying to unpack the return value into (out, code).
111 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000114def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000115 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000117 return (version.startswith(prefix) and
118 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000119
120
maruel@chromium.org90541732011-04-01 17:54:18 +0000121def ask_for_data(prompt):
122 try:
123 return raw_input(prompt)
124 except KeyboardInterrupt:
125 # Hide the exception.
126 sys.exit(1)
127
128
iannucci@chromium.org79540052012-10-19 23:15:26 +0000129def git_set_branch_value(key, value):
130 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000131 if not branch:
132 return
133
134 cmd = ['config']
135 if isinstance(value, int):
136 cmd.append('--int')
137 git_key = 'branch.%s.%s' % (branch, key)
138 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139
140
141def git_get_branch_default(key, default):
142 branch = Changelist().GetBranch()
143 if branch:
144 git_key = 'branch.%s.%s' % (branch, key)
145 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
146 try:
147 return int(stdout.strip())
148 except ValueError:
149 pass
150 return default
151
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153def add_git_similarity(parser):
154 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 help='Sets the percentage that a pair of files need to match in order to'
157 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158 parser.add_option(
159 '--find-copies', action='store_true',
160 help='Allows git to look for copies.')
161 parser.add_option(
162 '--no-find-copies', action='store_false', dest='find_copies',
163 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164
165 old_parser_args = parser.parse_args
166 def Parse(args):
167 options, args = old_parser_args(args)
168
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000171 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 print('Note: Saving similarity of %d%% in git config.'
173 % options.similarity)
174 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000175
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 options.similarity = max(0, min(options.similarity, 100))
177
178 if options.find_copies is None:
179 options.find_copies = bool(
180 git_get_branch_default('git-find-copies', True))
181 else:
182 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183
184 print('Using %d%% similarity for rename/copy detection. '
185 'Override with --similarity.' % options.similarity)
186
187 return options, args
188 parser.parse_args = Parse
189
190
ukai@chromium.org259e4682012-10-25 07:36:33 +0000191def is_dirty_git_tree(cmd):
192 # Make sure index is up-to-date before running diff-index.
193 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
194 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
195 if dirty:
196 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
197 print 'Uncommitted files: (git diff-index --name-status HEAD)'
198 print dirty[:4096]
199 if len(dirty) > 4096:
200 print '... (run "git diff-index --name-status HEAD" to see full output).'
201 return True
202 return False
203
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000204
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000205def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
206 """Return the corresponding git ref if |base_url| together with |glob_spec|
207 matches the full |url|.
208
209 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
210 """
211 fetch_suburl, as_ref = glob_spec.split(':')
212 if allow_wildcards:
213 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
214 if glob_match:
215 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
216 # "branches/{472,597,648}/src:refs/remotes/svn/*".
217 branch_re = re.escape(base_url)
218 if glob_match.group(1):
219 branch_re += '/' + re.escape(glob_match.group(1))
220 wildcard = glob_match.group(2)
221 if wildcard == '*':
222 branch_re += '([^/]*)'
223 else:
224 # Escape and replace surrounding braces with parentheses and commas
225 # with pipe symbols.
226 wildcard = re.escape(wildcard)
227 wildcard = re.sub('^\\\\{', '(', wildcard)
228 wildcard = re.sub('\\\\,', '|', wildcard)
229 wildcard = re.sub('\\\\}$', ')', wildcard)
230 branch_re += wildcard
231 if glob_match.group(3):
232 branch_re += re.escape(glob_match.group(3))
233 match = re.match(branch_re, url)
234 if match:
235 return re.sub('\*$', match.group(1), as_ref)
236
237 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
238 if fetch_suburl:
239 full_url = base_url + '/' + fetch_suburl
240 else:
241 full_url = base_url
242 if full_url == url:
243 return as_ref
244 return None
245
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000246
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 """Prints statistics about the change to the user."""
249 # --no-ext-diff is broken in some versions of Git, so try to work around
250 # this by overriding the environment (but there is still a problem if the
251 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000252 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000253 if 'GIT_EXTERNAL_DIFF' in env:
254 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255
256 if find_copies:
257 similarity_options = ['--find-copies-harder', '-l100000',
258 '-C%s' % similarity]
259 else:
260 similarity_options = ['-M%s' % similarity]
261
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000262 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000263 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000264 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000265 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000266
267
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268class Settings(object):
269 def __init__(self):
270 self.default_server = None
271 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000272 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000273 self.is_git_svn = None
274 self.svn_branch = None
275 self.tree_status_url = None
276 self.viewvc_url = None
277 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000278 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000279 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000280
281 def LazyUpdateIfNeeded(self):
282 """Updates the settings from a codereview.settings file, if available."""
283 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000284 # The only value that actually changes the behavior is
285 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000286 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000287 error_ok=True
288 ).strip().lower()
289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000291 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000293 # set updated to True to avoid infinite calling loop
294 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000295 self.updated = True
296 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 self.updated = True
298
299 def GetDefaultServerUrl(self, error_ok=False):
300 if not self.default_server:
301 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000302 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000303 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000304 if error_ok:
305 return self.default_server
306 if not self.default_server:
307 error_message = ('Could not find settings file. You must configure '
308 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000309 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000310 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000311 return self.default_server
312
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000313 @staticmethod
314 def GetRelativeRoot():
315 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000317 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000318 if self.root is None:
319 self.root = os.path.abspath(self.GetRelativeRoot())
320 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321
322 def GetIsGitSvn(self):
323 """Return true if this repo looks like it's using git-svn."""
324 if self.is_git_svn is None:
325 # If you have any "svn-remote.*" config keys, we think you're using svn.
326 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000327 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000328 return self.is_git_svn
329
330 def GetSVNBranch(self):
331 if self.svn_branch is None:
332 if not self.GetIsGitSvn():
333 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
334
335 # Try to figure out which remote branch we're based on.
336 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000337 # 1) iterate through our branch history and find the svn URL.
338 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000339
340 # regexp matching the git-svn line that contains the URL.
341 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
342
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000343 # We don't want to go through all of history, so read a line from the
344 # pipe at a time.
345 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000346 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000347 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
348 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000349 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000350 for line in proc.stdout:
351 match = git_svn_re.match(line)
352 if match:
353 url = match.group(1)
354 proc.stdout.close() # Cut pipe.
355 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000356
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000357 if url:
358 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
359 remotes = RunGit(['config', '--get-regexp',
360 r'^svn-remote\..*\.url']).splitlines()
361 for remote in remotes:
362 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000363 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000364 remote = match.group(1)
365 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000366 rewrite_root = RunGit(
367 ['config', 'svn-remote.%s.rewriteRoot' % remote],
368 error_ok=True).strip()
369 if rewrite_root:
370 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000371 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000372 ['config', 'svn-remote.%s.fetch' % remote],
373 error_ok=True).strip()
374 if fetch_spec:
375 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
376 if self.svn_branch:
377 break
378 branch_spec = RunGit(
379 ['config', 'svn-remote.%s.branches' % remote],
380 error_ok=True).strip()
381 if branch_spec:
382 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
383 if self.svn_branch:
384 break
385 tag_spec = RunGit(
386 ['config', 'svn-remote.%s.tags' % remote],
387 error_ok=True).strip()
388 if tag_spec:
389 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
390 if self.svn_branch:
391 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000392
393 if not self.svn_branch:
394 DieWithError('Can\'t guess svn branch -- try specifying it on the '
395 'command line')
396
397 return self.svn_branch
398
399 def GetTreeStatusUrl(self, error_ok=False):
400 if not self.tree_status_url:
401 error_message = ('You must configure your tree status URL by running '
402 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000403 self.tree_status_url = self._GetRietveldConfig(
404 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000405 return self.tree_status_url
406
407 def GetViewVCUrl(self):
408 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000409 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000410 return self.viewvc_url
411
rmistry@google.com90752582014-01-14 21:04:50 +0000412 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000413 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000414
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000415 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000416 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000417
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000418 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000419 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000420
ukai@chromium.orge8077812012-02-03 03:41:46 +0000421 def GetIsGerrit(self):
422 """Return true if this repo is assosiated with gerrit code review system."""
423 if self.is_gerrit is None:
424 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
425 return self.is_gerrit
426
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000427 def GetGitEditor(self):
428 """Return the editor specified in the git config, or None if none is."""
429 if self.git_editor is None:
430 self.git_editor = self._GetConfig('core.editor', error_ok=True)
431 return self.git_editor or None
432
thestig@chromium.org44202a22014-03-11 19:22:18 +0000433 def GetLintRegex(self):
434 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
435 DEFAULT_LINT_REGEX)
436
437 def GetLintIgnoreRegex(self):
438 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
439 DEFAULT_LINT_IGNORE_REGEX)
440
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000441 def _GetRietveldConfig(self, param, **kwargs):
442 return self._GetConfig('rietveld.' + param, **kwargs)
443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000444 def _GetConfig(self, param, **kwargs):
445 self.LazyUpdateIfNeeded()
446 return RunGit(['config', param], **kwargs).strip()
447
448
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449def ShortBranchName(branch):
450 """Convert a name like 'refs/heads/foo' to just 'foo'."""
451 return branch.replace('refs/heads/', '')
452
453
454class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000455 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000457 global settings
458 if not settings:
459 # Happens when git_cl.py is used as a utility library.
460 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000461 settings.GetDefaultServerUrl()
462 self.branchref = branchref
463 if self.branchref:
464 self.branch = ShortBranchName(self.branchref)
465 else:
466 self.branch = None
467 self.rietveld_server = None
468 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000469 self.lookedup_issue = False
470 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 self.has_description = False
472 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000473 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000474 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000475 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000476 self.cc = None
477 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000478 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000479 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000480
481 def GetCCList(self):
482 """Return the users cc'd on this CL.
483
484 Return is a string suitable for passing to gcl with the --cc flag.
485 """
486 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000487 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000488 more_cc = ','.join(self.watchers)
489 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
490 return self.cc
491
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000492 def GetCCListWithoutDefault(self):
493 """Return the users cc'd on this CL excluding default ones."""
494 if self.cc is None:
495 self.cc = ','.join(self.watchers)
496 return self.cc
497
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000498 def SetWatchers(self, watchers):
499 """Set the list of email addresses that should be cc'd based on the changed
500 files in this CL.
501 """
502 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503
504 def GetBranch(self):
505 """Returns the short branch name, e.g. 'master'."""
506 if not self.branch:
507 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
508 self.branch = ShortBranchName(self.branchref)
509 return self.branch
510
511 def GetBranchRef(self):
512 """Returns the full branch name, e.g. 'refs/heads/master'."""
513 self.GetBranch() # Poke the lazy loader.
514 return self.branchref
515
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000516 @staticmethod
517 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000518 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000519 e.g. 'origin', 'refs/heads/master'
520 """
521 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000522 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
523 error_ok=True).strip()
524 if upstream_branch:
525 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
526 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000527 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
528 error_ok=True).strip()
529 if upstream_branch:
530 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000531 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000532 # Fall back on trying a git-svn upstream branch.
533 if settings.GetIsGitSvn():
534 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000535 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000536 # Else, try to guess the origin remote.
537 remote_branches = RunGit(['branch', '-r']).split()
538 if 'origin/master' in remote_branches:
539 # Fall back on origin/master if it exits.
540 remote = 'origin'
541 upstream_branch = 'refs/heads/master'
542 elif 'origin/trunk' in remote_branches:
543 # Fall back on origin/trunk if it exists. Generally a shared
544 # git-svn clone
545 remote = 'origin'
546 upstream_branch = 'refs/heads/trunk'
547 else:
548 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549Either pass complete "git diff"-style arguments, like
550 git cl upload origin/master
551or verify this branch is set up to track another (via the --track argument to
552"git checkout -b ...").""")
553
554 return remote, upstream_branch
555
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000557 return git_common.get_or_create_merge_base(self.GetBranch(),
558 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000559
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000560 def GetUpstreamBranch(self):
561 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000562 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000563 if remote is not '.':
564 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
565 self.upstream_branch = upstream_branch
566 return self.upstream_branch
567
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000568 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000569 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000570 remote, branch = None, self.GetBranch()
571 seen_branches = set()
572 while branch not in seen_branches:
573 seen_branches.add(branch)
574 remote, branch = self.FetchUpstreamTuple(branch)
575 branch = ShortBranchName(branch)
576 if remote != '.' or branch.startswith('refs/remotes'):
577 break
578 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000579 remotes = RunGit(['remote'], error_ok=True).split()
580 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000581 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000582 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000583 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000584 logging.warning('Could not determine which remote this change is '
585 'associated with, so defaulting to "%s". This may '
586 'not be what you want. You may prevent this message '
587 'by running "git svn info" as documented here: %s',
588 self._remote,
589 GIT_INSTRUCTIONS_URL)
590 else:
591 logging.warn('Could not determine which remote this change is '
592 'associated with. You may prevent this message by '
593 'running "git svn info" as documented here: %s',
594 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000595 branch = 'HEAD'
596 if branch.startswith('refs/remotes'):
597 self._remote = (remote, branch)
598 else:
599 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000600 return self._remote
601
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000602 def GitSanityChecks(self, upstream_git_obj):
603 """Checks git repo status and ensures diff is from local commits."""
604
605 # Verify the commit we're diffing against is in our current branch.
606 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
607 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
608 if upstream_sha != common_ancestor:
609 print >> sys.stderr, (
610 'ERROR: %s is not in the current branch. You may need to rebase '
611 'your tracking branch' % upstream_sha)
612 return False
613
614 # List the commits inside the diff, and verify they are all local.
615 commits_in_diff = RunGit(
616 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
617 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
618 remote_branch = remote_branch.strip()
619 if code != 0:
620 _, remote_branch = self.GetRemoteBranch()
621
622 commits_in_remote = RunGit(
623 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
624
625 common_commits = set(commits_in_diff) & set(commits_in_remote)
626 if common_commits:
627 print >> sys.stderr, (
628 'ERROR: Your diff contains %d commits already in %s.\n'
629 'Run "git log --oneline %s..HEAD" to get a list of commits in '
630 'the diff. If you are using a custom git flow, you can override'
631 ' the reference used for this check with "git config '
632 'gitcl.remotebranch <git-ref>".' % (
633 len(common_commits), remote_branch, upstream_git_obj))
634 return False
635 return True
636
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000637 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000638 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000639
640 Returns None if it is not set.
641 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000642 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
643 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 def GetRemoteUrl(self):
646 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
647
648 Returns None if there is no remote.
649 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000650 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
652
653 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000654 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000655 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000657 self.issue = int(issue) or None if issue else None
658 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 return self.issue
660
661 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000662 if not self.rietveld_server:
663 # If we're on a branch then get the server potentially associated
664 # with that branch.
665 if self.GetIssue():
666 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
667 ['config', self._RietveldServer()], error_ok=True).strip())
668 if not self.rietveld_server:
669 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 return self.rietveld_server
671
672 def GetIssueURL(self):
673 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000674 if not self.GetIssue():
675 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
677
678 def GetDescription(self, pretty=False):
679 if not self.has_description:
680 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000681 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000682 try:
683 self.description = self.RpcServer().get_description(issue).strip()
684 except urllib2.HTTPError, e:
685 if e.code == 404:
686 DieWithError(
687 ('\nWhile fetching the description for issue %d, received a '
688 '404 (not found)\n'
689 'error. It is likely that you deleted this '
690 'issue on the server. If this is the\n'
691 'case, please run\n\n'
692 ' git cl issue 0\n\n'
693 'to clear the association with the deleted issue. Then run '
694 'this command again.') % issue)
695 else:
696 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000697 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698 self.has_description = True
699 if pretty:
700 wrapper = textwrap.TextWrapper()
701 wrapper.initial_indent = wrapper.subsequent_indent = ' '
702 return wrapper.fill(self.description)
703 return self.description
704
705 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000706 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000707 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 patchset = RunGit(['config', self._PatchsetSetting()],
709 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000710 self.patchset = int(patchset) or None if patchset else None
711 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712 return self.patchset
713
714 def SetPatchset(self, patchset):
715 """Set this branch's patchset. If patchset=0, clears the patchset."""
716 if patchset:
717 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000718 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 else:
720 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000721 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000722 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000724 def GetMostRecentPatchset(self):
725 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000726
727 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000728 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000729 '/download/issue%s_%s.diff' % (issue, patchset))
730
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000731 def GetIssueProperties(self):
732 if self._props is None:
733 issue = self.GetIssue()
734 if not issue:
735 self._props = {}
736 else:
737 self._props = self.RpcServer().get_issue_properties(issue, True)
738 return self._props
739
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000740 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000741 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 def SetIssue(self, issue):
744 """Set this branch's issue. If issue=0, clears the issue."""
745 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000746 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 RunGit(['config', self._IssueSetting(), str(issue)])
748 if self.rietveld_server:
749 RunGit(['config', self._RietveldServer(), self.rietveld_server])
750 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000751 current_issue = self.GetIssue()
752 if current_issue:
753 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000754 self.issue = None
755 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000756
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000757 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000758 if not self.GitSanityChecks(upstream_branch):
759 DieWithError('\nGit sanity check failure')
760
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000761 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000762 if not root:
763 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000764 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000765
766 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000767 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000768 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000769 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000770 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000771 except subprocess2.CalledProcessError:
772 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000773 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000774 'This branch probably doesn\'t exist anymore. To reset the\n'
775 'tracking branch, please run\n'
776 ' git branch --set-upstream %s trunk\n'
777 'replacing trunk with origin/master or the relevant branch') %
778 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000779
maruel@chromium.org52424302012-08-29 15:14:30 +0000780 issue = self.GetIssue()
781 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000782 if issue:
783 description = self.GetDescription()
784 else:
785 # If the change was never uploaded, use the log messages of all commits
786 # up to the branch point, as git cl upload will prefill the description
787 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000788 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
789 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000790
791 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000792 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000793 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000794 name,
795 description,
796 absroot,
797 files,
798 issue,
799 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000800 author,
801 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000802
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000803 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000804 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000805
806 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000807 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000808 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000809 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000810 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000811 except presubmit_support.PresubmitFailure, e:
812 DieWithError(
813 ('%s\nMaybe your depot_tools is out of date?\n'
814 'If all fails, contact maruel@') % e)
815
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000816 def UpdateDescription(self, description):
817 self.description = description
818 return self.RpcServer().update_description(
819 self.GetIssue(), self.description)
820
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000822 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000823 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000825 def SetFlag(self, flag, value):
826 """Patchset must match."""
827 if not self.GetPatchset():
828 DieWithError('The patchset needs to match. Send another patchset.')
829 try:
830 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000831 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000832 except urllib2.HTTPError, e:
833 if e.code == 404:
834 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
835 if e.code == 403:
836 DieWithError(
837 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
838 'match?') % (self.GetIssue(), self.GetPatchset()))
839 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000841 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 """Returns an upload.RpcServer() to access this review's rietveld instance.
843 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000844 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000845 self._rpc_server = rietveld.CachingRietveld(
846 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000847 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
849 def _IssueSetting(self):
850 """Return the git setting that stores this change's issue."""
851 return 'branch.%s.rietveldissue' % self.GetBranch()
852
853 def _PatchsetSetting(self):
854 """Return the git setting that stores this change's most recent patchset."""
855 return 'branch.%s.rietveldpatchset' % self.GetBranch()
856
857 def _RietveldServer(self):
858 """Returns the git setting that stores this change's rietveld server."""
859 return 'branch.%s.rietveldserver' % self.GetBranch()
860
861
862def GetCodereviewSettingsInteractively():
863 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000864 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 server = settings.GetDefaultServerUrl(error_ok=True)
866 prompt = 'Rietveld server (host[:port])'
867 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000868 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 if not server and not newserver:
870 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000871 if newserver:
872 newserver = gclient_utils.UpgradeToHttps(newserver)
873 if newserver != server:
874 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000876 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000877 prompt = caption
878 if initial:
879 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000880 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 if new_val == 'x':
882 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000883 elif new_val:
884 if is_url:
885 new_val = gclient_utils.UpgradeToHttps(new_val)
886 if new_val != initial:
887 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000889 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000890 SetProperty(settings.GetDefaultPrivateFlag(),
891 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000893 'tree-status-url', False)
894 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000895 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896
897 # TODO: configure a default branch to diff against, rather than this
898 # svn-based hackery.
899
900
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000901class ChangeDescription(object):
902 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000903 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000904 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000905
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000906 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000907 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000908
agable@chromium.org42c20792013-09-12 17:34:49 +0000909 @property # www.logilab.org/ticket/89786
910 def description(self): # pylint: disable=E0202
911 return '\n'.join(self._description_lines)
912
913 def set_description(self, desc):
914 if isinstance(desc, basestring):
915 lines = desc.splitlines()
916 else:
917 lines = [line.rstrip() for line in desc]
918 while lines and not lines[0]:
919 lines.pop(0)
920 while lines and not lines[-1]:
921 lines.pop(-1)
922 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000923
924 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000925 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000926 assert isinstance(reviewers, list), reviewers
927 if not reviewers:
928 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000929 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000930
agable@chromium.org42c20792013-09-12 17:34:49 +0000931 # Get the set of R= and TBR= lines and remove them from the desciption.
932 regexp = re.compile(self.R_LINE)
933 matches = [regexp.match(line) for line in self._description_lines]
934 new_desc = [l for i, l in enumerate(self._description_lines)
935 if not matches[i]]
936 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000937
agable@chromium.org42c20792013-09-12 17:34:49 +0000938 # Construct new unified R= and TBR= lines.
939 r_names = []
940 tbr_names = []
941 for match in matches:
942 if not match:
943 continue
944 people = cleanup_list([match.group(2).strip()])
945 if match.group(1) == 'TBR':
946 tbr_names.extend(people)
947 else:
948 r_names.extend(people)
949 for name in r_names:
950 if name not in reviewers:
951 reviewers.append(name)
952 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
953 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
954
955 # Put the new lines in the description where the old first R= line was.
956 line_loc = next((i for i, match in enumerate(matches) if match), -1)
957 if 0 <= line_loc < len(self._description_lines):
958 if new_tbr_line:
959 self._description_lines.insert(line_loc, new_tbr_line)
960 if new_r_line:
961 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000962 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 if new_r_line:
964 self.append_footer(new_r_line)
965 if new_tbr_line:
966 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000967
968 def prompt(self):
969 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000970 self.set_description([
971 '# Enter a description of the change.',
972 '# This will be displayed on the codereview site.',
973 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000974 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000975 '--------------------',
976 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000977
agable@chromium.org42c20792013-09-12 17:34:49 +0000978 regexp = re.compile(self.BUG_LINE)
979 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000980 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000981 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000982 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000983 if not content:
984 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000985 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000986
987 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000988 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
989 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000990 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000991 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000992
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000993 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000994 if self._description_lines:
995 # Add an empty line if either the last line or the new line isn't a tag.
996 last_line = self._description_lines[-1]
997 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
998 not presubmit_support.Change.TAG_LINE_RE.match(line)):
999 self._description_lines.append('')
1000 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001001
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001002 def get_reviewers(self):
1003 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001004 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1005 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001006 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001007
1008
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001009def get_approving_reviewers(props):
1010 """Retrieves the reviewers that approved a CL from the issue properties with
1011 messages.
1012
1013 Note that the list may contain reviewers that are not committer, thus are not
1014 considered by the CQ.
1015 """
1016 return sorted(
1017 set(
1018 message['sender']
1019 for message in props['messages']
1020 if message['approval'] and message['sender'] in props['reviewers']
1021 )
1022 )
1023
1024
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025def FindCodereviewSettingsFile(filename='codereview.settings'):
1026 """Finds the given file starting in the cwd and going up.
1027
1028 Only looks up to the top of the repository unless an
1029 'inherit-review-settings-ok' file exists in the root of the repository.
1030 """
1031 inherit_ok_file = 'inherit-review-settings-ok'
1032 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001033 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1035 root = '/'
1036 while True:
1037 if filename in os.listdir(cwd):
1038 if os.path.isfile(os.path.join(cwd, filename)):
1039 return open(os.path.join(cwd, filename))
1040 if cwd == root:
1041 break
1042 cwd = os.path.dirname(cwd)
1043
1044
1045def LoadCodereviewSettingsFromFile(fileobj):
1046 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001047 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 def SetProperty(name, setting, unset_error_ok=False):
1050 fullname = 'rietveld.' + name
1051 if setting in keyvals:
1052 RunGit(['config', fullname, keyvals[setting]])
1053 else:
1054 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1055
1056 SetProperty('server', 'CODE_REVIEW_SERVER')
1057 # Only server setting is required. Other settings can be absent.
1058 # In that case, we ignore errors raised during option deletion attempt.
1059 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001060 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1062 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001063 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001064 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1065 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001067 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001068 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1071 #should be of the form
1072 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1073 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1074 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1075 keyvals['ORIGIN_URL_CONFIG']])
1076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001078def urlretrieve(source, destination):
1079 """urllib is broken for SSL connections via a proxy therefore we
1080 can't use urllib.urlretrieve()."""
1081 with open(destination, 'w') as f:
1082 f.write(urllib2.urlopen(source).read())
1083
1084
ukai@chromium.org712d6102013-11-27 00:52:58 +00001085def hasSheBang(fname):
1086 """Checks fname is a #! script."""
1087 with open(fname) as f:
1088 return f.read(2).startswith('#!')
1089
1090
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001091def DownloadHooks(force):
1092 """downloads hooks
1093
1094 Args:
1095 force: True to update hooks. False to install hooks if not present.
1096 """
1097 if not settings.GetIsGerrit():
1098 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001099 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001100 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1101 if not os.access(dst, os.X_OK):
1102 if os.path.exists(dst):
1103 if not force:
1104 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001105 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001106 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001107 if not hasSheBang(dst):
1108 DieWithError('Not a script: %s\n'
1109 'You need to download from\n%s\n'
1110 'into .git/hooks/commit-msg and '
1111 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001112 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1113 except Exception:
1114 if os.path.exists(dst):
1115 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001116 DieWithError('\nFailed to download hooks.\n'
1117 'You need to download from\n%s\n'
1118 'into .git/hooks/commit-msg and '
1119 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001120
1121
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001122@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001124 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001126 parser.add_option('--activate-update', action='store_true',
1127 help='activate auto-updating [rietveld] section in '
1128 '.git/config')
1129 parser.add_option('--deactivate-update', action='store_true',
1130 help='deactivate auto-updating [rietveld] section in '
1131 '.git/config')
1132 options, args = parser.parse_args(args)
1133
1134 if options.deactivate_update:
1135 RunGit(['config', 'rietveld.autoupdate', 'false'])
1136 return
1137
1138 if options.activate_update:
1139 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1140 return
1141
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 if len(args) == 0:
1143 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001144 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 return 0
1146
1147 url = args[0]
1148 if not url.endswith('codereview.settings'):
1149 url = os.path.join(url, 'codereview.settings')
1150
1151 # Load code review settings and download hooks (if available).
1152 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001153 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 return 0
1155
1156
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001157def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001158 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001159 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1160 branch = ShortBranchName(branchref)
1161 _, args = parser.parse_args(args)
1162 if not args:
1163 print("Current base-url:")
1164 return RunGit(['config', 'branch.%s.base-url' % branch],
1165 error_ok=False).strip()
1166 else:
1167 print("Setting base-url to %s" % args[0])
1168 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1169 error_ok=False).strip()
1170
1171
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001173 """Show status of changelists.
1174
1175 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001176 - Red not sent for review or broken
1177 - Blue waiting for review
1178 - Yellow waiting for you to reply to review
1179 - Green LGTM'ed
1180 - Magenta in the commit queue
1181 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001182
1183 Also see 'git cl comments'.
1184 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 parser.add_option('--field',
1186 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001187 parser.add_option('-f', '--fast', action='store_true',
1188 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001190 if args:
1191 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001194 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 if options.field.startswith('desc'):
1196 print cl.GetDescription()
1197 elif options.field == 'id':
1198 issueid = cl.GetIssue()
1199 if issueid:
1200 print issueid
1201 elif options.field == 'patch':
1202 patchset = cl.GetPatchset()
1203 if patchset:
1204 print patchset
1205 elif options.field == 'url':
1206 url = cl.GetIssueURL()
1207 if url:
1208 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001209 return 0
1210
1211 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1212 if not branches:
1213 print('No local branch found.')
1214 return 0
1215
1216 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001217 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001218 alignment = max(5, max(len(b) for b in branches))
1219 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001220 # Adhoc thread pool to request data concurrently.
1221 output = Queue.Queue()
1222
1223 # Silence upload.py otherwise it becomes unweldly.
1224 upload.verbosity = 0
1225
1226 if not options.fast:
1227 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001228 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 c = Changelist(branchref=b)
1230 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001231 props = {}
1232 r = None
1233 if i:
1234 try:
1235 props = c.GetIssueProperties()
1236 r = c.GetApprovingReviewers() if i else None
1237 except urllib2.HTTPError:
1238 # The issue probably doesn't exist anymore.
1239 i += ' (broken)'
1240
1241 msgs = props.get('messages') or []
1242
1243 if not i:
1244 color = Fore.WHITE
1245 elif props.get('closed'):
1246 # Issue is closed.
1247 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001248 elif props.get('commit'):
1249 # Issue is in the commit queue.
1250 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001251 elif r:
1252 # Was LGTM'ed.
1253 color = Fore.GREEN
1254 elif not msgs:
1255 # No message was sent.
1256 color = Fore.RED
1257 elif msgs[-1]['sender'] != props.get('owner_email'):
1258 color = Fore.YELLOW
1259 else:
1260 color = Fore.BLUE
1261 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001262
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001263 # Process one branch synchronously to work through authentication, then
1264 # spawn threads to process all the other branches in parallel.
1265 if branches:
1266 fetch(branches[0])
1267 threads = [
1268 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001269 for t in threads:
1270 t.daemon = True
1271 t.start()
1272 else:
1273 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1274 for b in branches:
1275 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001276 url = c.GetIssueURL()
1277 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001278
1279 tmp = {}
1280 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001281 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001283 b, i, color = output.get()
1284 tmp[b] = (i, color)
1285 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001286 reset = Fore.RESET
1287 if not sys.stdout.isatty():
1288 color = ''
1289 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001290 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001291 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001292
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001293 cl = Changelist()
1294 print
1295 print 'Current branch:',
1296 if not cl.GetIssue():
1297 print 'no issue assigned.'
1298 return 0
1299 print cl.GetBranch()
1300 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1301 print 'Issue description:'
1302 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 return 0
1304
1305
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001306def colorize_CMDstatus_doc():
1307 """To be called once in main() to add colors to git cl status help."""
1308 colors = [i for i in dir(Fore) if i[0].isupper()]
1309
1310 def colorize_line(line):
1311 for color in colors:
1312 if color in line.upper():
1313 # Extract whitespaces first and the leading '-'.
1314 indent = len(line) - len(line.lstrip(' ')) + 1
1315 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1316 return line
1317
1318 lines = CMDstatus.__doc__.splitlines()
1319 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1320
1321
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001322@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001324 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325
1326 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001327 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001328 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329
1330 cl = Changelist()
1331 if len(args) > 0:
1332 try:
1333 issue = int(args[0])
1334 except ValueError:
1335 DieWithError('Pass a number to set the issue or none to list it.\n'
1336 'Maybe you want to run git cl status?')
1337 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001338 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 return 0
1340
1341
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001342def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001343 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001344 (_, args) = parser.parse_args(args)
1345 if args:
1346 parser.error('Unsupported argument: %s' % args)
1347
1348 cl = Changelist()
1349 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001350 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001351 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001352 if message['disapproval']:
1353 color = Fore.RED
1354 elif message['approval']:
1355 color = Fore.GREEN
1356 elif message['sender'] == data['owner_email']:
1357 color = Fore.MAGENTA
1358 else:
1359 color = Fore.BLUE
1360 print '\n%s%s %s%s' % (
1361 color, message['date'].split('.', 1)[0], message['sender'],
1362 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001363 if message['text'].strip():
1364 print '\n'.join(' ' + l for l in message['text'].splitlines())
1365 return 0
1366
1367
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001368def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001369 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001370 cl = Changelist()
1371 if not cl.GetIssue():
1372 DieWithError('This branch has no associated changelist.')
1373 description = ChangeDescription(cl.GetDescription())
1374 description.prompt()
1375 cl.UpdateDescription(description.description)
1376 return 0
1377
1378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379def CreateDescriptionFromLog(args):
1380 """Pulls out the commit log to use as a base for the CL description."""
1381 log_args = []
1382 if len(args) == 1 and not args[0].endswith('.'):
1383 log_args = [args[0] + '..']
1384 elif len(args) == 1 and args[0].endswith('...'):
1385 log_args = [args[0][:-1]]
1386 elif len(args) == 2:
1387 log_args = [args[0] + '..' + args[1]]
1388 else:
1389 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001390 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001391
1392
thestig@chromium.org44202a22014-03-11 19:22:18 +00001393def CMDlint(parser, args):
1394 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001395 parser.add_option('--filter', action='append', metavar='-x,+y',
1396 help='Comma-separated list of cpplint\'s category-filters')
1397 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001398
1399 # Access to a protected member _XX of a client class
1400 # pylint: disable=W0212
1401 try:
1402 import cpplint
1403 import cpplint_chromium
1404 except ImportError:
1405 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1406 return 1
1407
1408 # Change the current working directory before calling lint so that it
1409 # shows the correct base.
1410 previous_cwd = os.getcwd()
1411 os.chdir(settings.GetRoot())
1412 try:
1413 cl = Changelist()
1414 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1415 files = [f.LocalPath() for f in change.AffectedFiles()]
1416
1417 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001418 command = args + files
1419 if options.filter:
1420 command = ['--filter=' + ','.join(options.filter)] + command
1421 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001422
1423 white_regex = re.compile(settings.GetLintRegex())
1424 black_regex = re.compile(settings.GetLintIgnoreRegex())
1425 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1426 for filename in filenames:
1427 if white_regex.match(filename):
1428 if black_regex.match(filename):
1429 print "Ignoring file %s" % filename
1430 else:
1431 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1432 extra_check_functions)
1433 else:
1434 print "Skipping file %s" % filename
1435 finally:
1436 os.chdir(previous_cwd)
1437 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1438 if cpplint._cpplint_state.error_count != 0:
1439 return 1
1440 return 0
1441
1442
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001444 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001445 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001447 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001448 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449 (options, args) = parser.parse_args(args)
1450
ukai@chromium.org259e4682012-10-25 07:36:33 +00001451 if not options.force and is_dirty_git_tree('presubmit'):
1452 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 return 1
1454
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001455 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 if args:
1457 base_branch = args[0]
1458 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001459 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001460 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001462 cl.RunHook(
1463 committing=not options.upload,
1464 may_prompt=False,
1465 verbose=options.verbose,
1466 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001467 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
1469
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001470def AddChangeIdToCommitMessage(options, args):
1471 """Re-commits using the current message, assumes the commit hook is in
1472 place.
1473 """
1474 log_desc = options.message or CreateDescriptionFromLog(args)
1475 git_command = ['commit', '--amend', '-m', log_desc]
1476 RunGit(git_command)
1477 new_log_desc = CreateDescriptionFromLog(args)
1478 if CHANGE_ID in new_log_desc:
1479 print 'git-cl: Added Change-Id to commit message.'
1480 else:
1481 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1482
1483
ukai@chromium.orge8077812012-02-03 03:41:46 +00001484def GerritUpload(options, args, cl):
1485 """upload the current branch to gerrit."""
1486 # We assume the remote called "origin" is the one we want.
1487 # It is probably not worthwhile to support different workflows.
1488 remote = 'origin'
1489 branch = 'master'
1490 if options.target_branch:
1491 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001493 change_desc = ChangeDescription(
1494 options.message or CreateDescriptionFromLog(args))
1495 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001496 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001498 if CHANGE_ID not in change_desc.description:
1499 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001500
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001501 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001502 if len(commits) > 1:
1503 print('WARNING: This will upload %d commits. Run the following command '
1504 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001505 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001506 print('You can also use `git squash-branch` to squash these into a single'
1507 'commit.')
1508 ask_for_data('About to upload; enter to confirm.')
1509
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001510 if options.reviewers:
1511 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001512
ukai@chromium.orge8077812012-02-03 03:41:46 +00001513 receive_options = []
1514 cc = cl.GetCCList().split(',')
1515 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001516 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001517 cc = filter(None, cc)
1518 if cc:
1519 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001520 if change_desc.get_reviewers():
1521 receive_options.extend(
1522 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001523
ukai@chromium.orge8077812012-02-03 03:41:46 +00001524 git_command = ['push']
1525 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001526 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001527 ' '.join(receive_options))
1528 git_command += [remote, 'HEAD:refs/for/' + branch]
1529 RunGit(git_command)
1530 # TODO(ukai): parse Change-Id: and set issue number?
1531 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533
ukai@chromium.orge8077812012-02-03 03:41:46 +00001534def RietveldUpload(options, args, cl):
1535 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1537 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001538 if options.emulate_svn_auto_props:
1539 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540
1541 change_desc = None
1542
pgervais@chromium.org91141372014-01-09 23:27:20 +00001543 if options.email is not None:
1544 upload_args.extend(['--email', options.email])
1545
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001547 if options.title:
1548 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001549 if options.message:
1550 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001551 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 print ("This branch is associated with issue %s. "
1553 "Adding patch to that issue." % cl.GetIssue())
1554 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001555 if options.title:
1556 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001557 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001558 change_desc = ChangeDescription(message)
1559 if options.reviewers:
1560 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001561 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001562 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001563
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001564 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565 print "Description is empty; aborting."
1566 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001567
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001568 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001569 if change_desc.get_reviewers():
1570 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001571 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001572 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001573 DieWithError("Must specify reviewers to send email.")
1574 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001575
1576 # We check this before applying rietveld.private assuming that in
1577 # rietveld.cc only addresses which we can send private CLs to are listed
1578 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1579 # --private is specified explicitly on the command line.
1580 if options.private:
1581 logging.warn('rietveld.cc is ignored since private flag is specified. '
1582 'You need to review and add them manually if necessary.')
1583 cc = cl.GetCCListWithoutDefault()
1584 else:
1585 cc = cl.GetCCList()
1586 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001587 if cc:
1588 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001590 if options.private or settings.GetDefaultPrivateFlag() == "True":
1591 upload_args.append('--private')
1592
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001593 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001594 if not options.find_copies:
1595 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001596
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 # Include the upstream repo's URL in the change -- this is useful for
1598 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001599 remote_url = cl.GetGitBaseUrlFromConfig()
1600 if not remote_url:
1601 if settings.GetIsGitSvn():
1602 # URL is dependent on the current directory.
1603 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1604 if data:
1605 keys = dict(line.split(': ', 1) for line in data.splitlines()
1606 if ': ' in line)
1607 remote_url = keys.get('URL', None)
1608 else:
1609 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1610 remote_url = (cl.GetRemoteUrl() + '@'
1611 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001612 if remote_url:
1613 upload_args.extend(['--base_url', remote_url])
1614
1615 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001616 upload_args = ['upload'] + upload_args + args
1617 logging.info('upload.RealMain(%s)', upload_args)
1618 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001619 issue = int(issue)
1620 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001621 except KeyboardInterrupt:
1622 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623 except:
1624 # If we got an exception after the user typed a description for their
1625 # change, back up the description before re-raising.
1626 if change_desc:
1627 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1628 print '\nGot exception while uploading -- saving description to %s\n' \
1629 % backup_path
1630 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001631 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632 backup_file.close()
1633 raise
1634
1635 if not cl.GetIssue():
1636 cl.SetIssue(issue)
1637 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001638
1639 if options.use_commit_queue:
1640 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641 return 0
1642
1643
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001644def cleanup_list(l):
1645 """Fixes a list so that comma separated items are put as individual items.
1646
1647 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1648 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1649 """
1650 items = sum((i.split(',') for i in l), [])
1651 stripped_items = (i.strip() for i in items)
1652 return sorted(filter(None, stripped_items))
1653
1654
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001655@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001656def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001657 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001658 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1659 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001660 parser.add_option('--bypass-watchlists', action='store_true',
1661 dest='bypass_watchlists',
1662 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001663 parser.add_option('-f', action='store_true', dest='force',
1664 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001665 parser.add_option('-m', dest='message', help='message for patchset')
1666 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001667 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001668 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001669 help='reviewer email addresses')
1670 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001671 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001672 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001673 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001674 help='send email to reviewer immediately')
1675 parser.add_option("--emulate_svn_auto_props", action="store_true",
1676 dest="emulate_svn_auto_props",
1677 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001678 parser.add_option('-c', '--use-commit-queue', action='store_true',
1679 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001680 parser.add_option('--private', action='store_true',
1681 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001682 parser.add_option('--target_branch',
1683 help='When uploading to gerrit, remote branch to '
1684 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001685 parser.add_option('--email', default=None,
1686 help='email address to use to connect to Rietveld')
1687
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001688 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001689 (options, args) = parser.parse_args(args)
1690
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001691 if options.target_branch and not settings.GetIsGerrit():
1692 parser.error('Use --target_branch for non gerrit repository.')
1693
ukai@chromium.org259e4682012-10-25 07:36:33 +00001694 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001695 return 1
1696
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001697 options.reviewers = cleanup_list(options.reviewers)
1698 options.cc = cleanup_list(options.cc)
1699
ukai@chromium.orge8077812012-02-03 03:41:46 +00001700 cl = Changelist()
1701 if args:
1702 # TODO(ukai): is it ok for gerrit case?
1703 base_branch = args[0]
1704 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001705 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001706 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001707 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001708
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001709 # Apply watchlists on upload.
1710 change = cl.GetChange(base_branch, None)
1711 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1712 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001713 if not options.bypass_watchlists:
1714 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001715
ukai@chromium.orge8077812012-02-03 03:41:46 +00001716 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001717 if options.reviewers:
1718 # Set the reviewer list now so that presubmit checks can access it.
1719 change_description = ChangeDescription(change.FullDescriptionText())
1720 change_description.update_reviewers(options.reviewers)
1721 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001722 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001723 may_prompt=not options.force,
1724 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001725 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001726 if not hook_results.should_continue():
1727 return 1
1728 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001729 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001730
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001731 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001732 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001733 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001734 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001735 print ('The last upload made from this repository was patchset #%d but '
1736 'the most recent patchset on the server is #%d.'
1737 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001738 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1739 'from another machine or branch the patch you\'re uploading now '
1740 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001741 ask_for_data('About to upload; enter to confirm.')
1742
iannucci@chromium.org79540052012-10-19 23:15:26 +00001743 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001744 if settings.GetIsGerrit():
1745 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001746 ret = RietveldUpload(options, args, cl)
1747 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001748 git_set_branch_value('last-upload-hash',
1749 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001750
1751 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001752
1753
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001754def IsSubmoduleMergeCommit(ref):
1755 # When submodules are added to the repo, we expect there to be a single
1756 # non-git-svn merge commit at remote HEAD with a signature comment.
1757 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001758 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001759 return RunGit(cmd) != ''
1760
1761
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001763 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001765 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766 Updates changelog with metadata (e.g. pointer to review).
1767 Pushes/dcommits the code upstream.
1768 Updates review and closes.
1769 """
1770 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1771 help='bypass upload presubmit hook')
1772 parser.add_option('-m', dest='message',
1773 help="override review description")
1774 parser.add_option('-f', action='store_true', dest='force',
1775 help="force yes to questions (don't prompt)")
1776 parser.add_option('-c', dest='contributor',
1777 help="external contributor for patch (appended to " +
1778 "description and used as author for git). Should be " +
1779 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001780 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781 (options, args) = parser.parse_args(args)
1782 cl = Changelist()
1783
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001784 current = cl.GetBranch()
1785 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1786 if not settings.GetIsGitSvn() and remote == '.':
1787 print
1788 print 'Attempting to push branch %r into another local branch!' % current
1789 print
1790 print 'Either reparent this branch on top of origin/master:'
1791 print ' git reparent-branch --root'
1792 print
1793 print 'OR run `git rebase-update` if you think the parent branch is already'
1794 print 'committed.'
1795 print
1796 print ' Current parent: %r' % upstream_branch
1797 return 1
1798
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001799 if not args or cmd == 'push':
1800 # Default to merging against our best guess of the upstream branch.
1801 args = [cl.GetUpstreamBranch()]
1802
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001803 if options.contributor:
1804 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1805 print "Please provide contibutor as 'First Last <email@example.com>'"
1806 return 1
1807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001809 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810
ukai@chromium.org259e4682012-10-25 07:36:33 +00001811 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812 return 1
1813
1814 # This rev-list syntax means "show all commits not in my branch that
1815 # are in base_branch".
1816 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1817 base_branch]).splitlines()
1818 if upstream_commits:
1819 print ('Base branch "%s" has %d commits '
1820 'not in this branch.' % (base_branch, len(upstream_commits)))
1821 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1822 return 1
1823
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001824 # This is the revision `svn dcommit` will commit on top of.
1825 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1826 '--pretty=format:%H'])
1827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001829 # If the base_head is a submodule merge commit, the first parent of the
1830 # base_head should be a git-svn commit, which is what we're interested in.
1831 base_svn_head = base_branch
1832 if base_has_submodules:
1833 base_svn_head += '^1'
1834
1835 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 if extra_commits:
1837 print ('This branch has %d additional commits not upstreamed yet.'
1838 % len(extra_commits.splitlines()))
1839 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1840 'before attempting to %s.' % (base_branch, cmd))
1841 return 1
1842
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001843 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001844 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001845 author = None
1846 if options.contributor:
1847 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001848 hook_results = cl.RunHook(
1849 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001850 may_prompt=not options.force,
1851 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001852 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001853 if not hook_results.should_continue():
1854 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855
1856 if cmd == 'dcommit':
1857 # Check the tree status if the tree status URL is set.
1858 status = GetTreeStatus()
1859 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001860 print('The tree is closed. Please wait for it to reopen. Use '
1861 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001862 return 1
1863 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001864 print('Unable to determine tree status. Please verify manually and '
1865 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001866 else:
1867 breakpad.SendStack(
1868 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001869 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1870 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001871 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001873 change_desc = ChangeDescription(options.message)
1874 if not change_desc.description and cl.GetIssue():
1875 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001877 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001878 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001879 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001880 else:
1881 print 'No description set.'
1882 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1883 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001885 # Keep a separate copy for the commit message, because the commit message
1886 # contains the link to the Rietveld issue, while the Rietveld message contains
1887 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001888 # Keep a separate copy for the commit message.
1889 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001890 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001891
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001892 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001893 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001894 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001895 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001896 commit_desc.append_footer('Patch from %s.' % options.contributor)
1897
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001898 print('Description:')
1899 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001900
1901 branches = [base_branch, cl.GetBranchRef()]
1902 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001903 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001904 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001906 # We want to squash all this branch's commits into one commit with the proper
1907 # description. We do this by doing a "reset --soft" to the base branch (which
1908 # keeps the working copy the same), then dcommitting that. If origin/master
1909 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1910 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001912 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1913 # Delete the branches if they exist.
1914 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1915 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1916 result = RunGitWithCode(showref_cmd)
1917 if result[0] == 0:
1918 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919
1920 # We might be in a directory that's present in this branch but not in the
1921 # trunk. Move up to the top of the tree so that git commands that expect a
1922 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001923 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924 if rel_base_path:
1925 os.chdir(rel_base_path)
1926
1927 # Stuff our change into the merge branch.
1928 # We wrap in a try...finally block so if anything goes wrong,
1929 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001930 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001932 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1933 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001935 RunGit(
1936 [
1937 'commit', '--author', options.contributor,
1938 '-m', commit_desc.description,
1939 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001941 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001942 if base_has_submodules:
1943 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1944 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1945 RunGit(['checkout', CHERRY_PICK_BRANCH])
1946 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947 if cmd == 'push':
1948 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001949 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950 retcode, output = RunGitWithCode(
1951 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1952 logging.debug(output)
1953 else:
1954 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001955 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001956 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001957 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 finally:
1959 # And then swap back to the original branch and clean up.
1960 RunGit(['checkout', '-q', cl.GetBranch()])
1961 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001962 if base_has_submodules:
1963 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964
1965 if cl.GetIssue():
1966 if cmd == 'dcommit' and 'Committed r' in output:
1967 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1968 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001969 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1970 for l in output.splitlines(False))
1971 match = filter(None, match)
1972 if len(match) != 1:
1973 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1974 output)
1975 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 else:
1977 return 1
1978 viewvc_url = settings.GetViewVCUrl()
1979 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001980 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001981 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001982 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983 print ('Closing issue '
1984 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001985 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001987 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001988 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001989 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001990 if options.bypass_hooks:
1991 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1992 else:
1993 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001994 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001995 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001996
1997 if retcode == 0:
1998 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1999 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002000 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002 return 0
2003
2004
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002005@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002007 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002008 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002009 message = """This doesn't appear to be an SVN repository.
2010If your project has a git mirror with an upstream SVN master, you probably need
2011to run 'git svn init', see your project's git mirror documentation.
2012If your project has a true writeable upstream repository, you probably want
2013to run 'git cl push' instead.
2014Choose wisely, if you get this wrong, your commit might appear to succeed but
2015will instead be silently ignored."""
2016 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002017 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018 return SendUpstream(parser, args, 'dcommit')
2019
2020
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002021@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002022def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002023 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024 if settings.GetIsGitSvn():
2025 print('This appears to be an SVN repository.')
2026 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002027 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028 return SendUpstream(parser, args, 'push')
2029
2030
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002031@subcommand.usage('[upstream branch to apply against]')
2032def CMDpush(parser, args):
2033 """Temporary alias for 'land'.
2034 """
2035 print(
2036 "\n=======\n"
2037 "'git cl push' has been renamed to 'git cl land'.\n"
2038 "Currently they are treated as synonyms, but 'git cl push' will stop\n"
2039 "working after 2014/07/01.\n"
2040 "=======\n")
2041 now = datetime.datetime.utcfromtimestamp(time.time())
2042 if now > datetime.datetime(2014, 7, 1):
2043 print('******\nReally, you should not use this command anymore... \n'
2044 'Pausing 10 sec to help you remember :-)')
2045 for n in xrange(10):
2046 time.sleep(1)
2047 print('%s seconds...' % (n+1))
2048 print('******')
2049 return CMDland(parser, args)
2050
2051
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002052@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002054 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055 parser.add_option('-b', dest='newbranch',
2056 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002057 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002059 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2060 help='Change to the directory DIR immediately, '
2061 'before doing anything else.')
2062 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002063 help='failed patches spew .rej files rather than '
2064 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2066 help="don't commit after patch applies")
2067 (options, args) = parser.parse_args(args)
2068 if len(args) != 1:
2069 parser.print_help()
2070 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002071 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002073 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002074 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002075
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002076 if options.newbranch:
2077 if options.force:
2078 RunGit(['branch', '-D', options.newbranch],
2079 stderr=subprocess2.PIPE, error_ok=True)
2080 RunGit(['checkout', '-b', options.newbranch,
2081 Changelist().GetUpstreamBranch()])
2082
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002083 return PatchIssue(issue_arg, options.reject, options.nocommit,
2084 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002085
2086
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002087def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002088 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002090 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002091 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002092 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002093 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002094 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002095 # Assume it's a URL to the patch. Default to https.
2096 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002097 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002098 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099 DieWithError('Must pass an issue ID or full URL for '
2100 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002101 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002102 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002103 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002105 # Switch up to the top-level directory, if necessary, in preparation for
2106 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002107 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108 if top:
2109 os.chdir(top)
2110
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 # Git patches have a/ at the beginning of source paths. We strip that out
2112 # with a sed script rather than the -p flag to patch so we can feed either
2113 # Git or svn-style patches into the same apply command.
2114 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002115 try:
2116 patch_data = subprocess2.check_output(
2117 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2118 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 DieWithError('Git patch mungling failed.')
2120 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 # We use "git apply" to apply the patch instead of "patch" so that we can
2123 # pick up file adds.
2124 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002125 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002126 if directory:
2127 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002128 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002130 elif IsGitVersionAtLeast('1.7.12'):
2131 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002132 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002133 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002134 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002135 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 DieWithError('Failed to apply the patch')
2137
2138 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002139 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002140 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2141 cl = Changelist()
2142 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002143 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002144 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002145 else:
2146 print "Patch applied to index."
2147 return 0
2148
2149
2150def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002151 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 # Provide a wrapper for git svn rebase to help avoid accidental
2153 # git svn dcommit.
2154 # It's the only command that doesn't use parser at all since we just defer
2155 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002156
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002157 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158
2159
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002160def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002161 """Fetches the tree status and returns either 'open', 'closed',
2162 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002163 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002164 if url:
2165 status = urllib2.urlopen(url).read().lower()
2166 if status.find('closed') != -1 or status == '0':
2167 return 'closed'
2168 elif status.find('open') != -1 or status == '1':
2169 return 'open'
2170 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002171 return 'unset'
2172
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002173
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002174def GetTreeStatusReason():
2175 """Fetches the tree status from a json url and returns the message
2176 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002177 url = settings.GetTreeStatusUrl()
2178 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002179 connection = urllib2.urlopen(json_url)
2180 status = json.loads(connection.read())
2181 connection.close()
2182 return status['message']
2183
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002185def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002186 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002187 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002188 status = GetTreeStatus()
2189 if 'unset' == status:
2190 print 'You must configure your tree status URL by running "git cl config".'
2191 return 2
2192
2193 print "The tree is %s" % status
2194 print
2195 print GetTreeStatusReason()
2196 if status != 'open':
2197 return 1
2198 return 0
2199
2200
maruel@chromium.org15192402012-09-06 12:38:29 +00002201def CMDtry(parser, args):
2202 """Triggers a try job through Rietveld."""
2203 group = optparse.OptionGroup(parser, "Try job options")
2204 group.add_option(
2205 "-b", "--bot", action="append",
2206 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2207 "times to specify multiple builders. ex: "
2208 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2209 "the try server waterfall for the builders name and the tests "
2210 "available. Can also be used to specify gtest_filter, e.g. "
2211 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2212 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002213 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002214 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002215 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002216 "-r", "--revision",
2217 help="Revision to use for the try job; default: the "
2218 "revision will be determined by the try server; see "
2219 "its waterfall for more info")
2220 group.add_option(
2221 "-c", "--clobber", action="store_true", default=False,
2222 help="Force a clobber before building; e.g. don't do an "
2223 "incremental build")
2224 group.add_option(
2225 "--project",
2226 help="Override which project to use. Projects are defined "
2227 "server-side to define what default bot set to use")
2228 group.add_option(
2229 "-t", "--testfilter", action="append", default=[],
2230 help=("Apply a testfilter to all the selected builders. Unless the "
2231 "builders configurations are similar, use multiple "
2232 "--bot <builder>:<test> arguments."))
2233 group.add_option(
2234 "-n", "--name", help="Try job name; default to current branch name")
2235 parser.add_option_group(group)
2236 options, args = parser.parse_args(args)
2237
2238 if args:
2239 parser.error('Unknown arguments: %s' % args)
2240
2241 cl = Changelist()
2242 if not cl.GetIssue():
2243 parser.error('Need to upload first')
2244
2245 if not options.name:
2246 options.name = cl.GetBranch()
2247
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002248 if options.bot and not options.master:
2249 parser.error('For manually specified bots please also specify the '
2250 'tryserver master, e.g. "-m tryserver.chromium".')
2251
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002252 def GetMasterMap():
2253 # Process --bot and --testfilter.
2254 if not options.bot:
2255 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002256
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002257 # Get try masters from PRESUBMIT.py files.
2258 masters = presubmit_support.DoGetTryMasters(
2259 change,
2260 change.LocalPaths(),
2261 settings.GetRoot(),
2262 None,
2263 None,
2264 options.verbose,
2265 sys.stdout)
2266 if masters:
2267 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002268
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002269 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2270 options.bot = presubmit_support.DoGetTrySlaves(
2271 change,
2272 change.LocalPaths(),
2273 settings.GetRoot(),
2274 None,
2275 None,
2276 options.verbose,
2277 sys.stdout)
2278 if not options.bot:
2279 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002280
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002281 builders_and_tests = {}
2282 # TODO(machenbach): The old style command-line options don't support
2283 # multiple try masters yet.
2284 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2285 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2286
2287 for bot in old_style:
2288 if ':' in bot:
2289 builder, tests = bot.split(':', 1)
2290 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2291 elif ',' in bot:
2292 parser.error('Specify one bot per --bot flag')
2293 else:
2294 builders_and_tests.setdefault(bot, []).append('defaulttests')
2295
2296 for bot, tests in new_style:
2297 builders_and_tests.setdefault(bot, []).extend(tests)
2298
2299 # Return a master map with one master to be backwards compatible. The
2300 # master name defaults to an empty string, which will cause the master
2301 # not to be set on rietveld (deprecated).
2302 return {options.master: builders_and_tests}
2303
2304 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002305
maruel@chromium.org15192402012-09-06 12:38:29 +00002306 if options.testfilter:
2307 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002308 masters = dict((master, dict(
2309 (b, forced_tests) for b, t in slaves.iteritems()
2310 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002311
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002312 for builders in masters.itervalues():
2313 if any('triggered' in b for b in builders):
2314 print >> sys.stderr, (
2315 'ERROR You are trying to send a job to a triggered bot. This type of'
2316 ' bot requires an\ninitial job from a parent (usually a builder). '
2317 'Instead send your job to the parent.\n'
2318 'Bot list: %s' % builders)
2319 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002320
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002321 patchset = cl.GetMostRecentPatchset()
2322 if patchset and patchset != cl.GetPatchset():
2323 print(
2324 '\nWARNING Mismatch between local config and server. Did a previous '
2325 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2326 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002327 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002328 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002329 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002330 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002331 except urllib2.HTTPError, e:
2332 if e.code == 404:
2333 print('404 from rietveld; '
2334 'did you mean to use "git try" instead of "git cl try"?')
2335 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002336 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002337
2338 for (master, builders) in masters.iteritems():
2339 if master:
2340 print 'Master: %s' % master
2341 length = max(len(builder) for builder in builders)
2342 for builder in sorted(builders):
2343 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002344 return 0
2345
2346
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002347@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002348def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002349 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002350 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002351 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002352 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002353
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002354 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002355 if args:
2356 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002357 branch = cl.GetBranch()
2358 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002359 cl = Changelist()
2360 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002361
2362 # Clear configured merge-base, if there is one.
2363 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002364 else:
2365 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002366 return 0
2367
2368
thestig@chromium.org00858c82013-12-02 23:08:03 +00002369def CMDweb(parser, args):
2370 """Opens the current CL in the web browser."""
2371 _, args = parser.parse_args(args)
2372 if args:
2373 parser.error('Unrecognized args: %s' % ' '.join(args))
2374
2375 issue_url = Changelist().GetIssueURL()
2376 if not issue_url:
2377 print >> sys.stderr, 'ERROR No issue to open'
2378 return 1
2379
2380 webbrowser.open(issue_url)
2381 return 0
2382
2383
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002384def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002385 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002386 _, args = parser.parse_args(args)
2387 if args:
2388 parser.error('Unrecognized args: %s' % ' '.join(args))
2389 cl = Changelist()
2390 cl.SetFlag('commit', '1')
2391 return 0
2392
2393
groby@chromium.org411034a2013-02-26 15:12:01 +00002394def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002395 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002396 _, args = parser.parse_args(args)
2397 if args:
2398 parser.error('Unrecognized args: %s' % ' '.join(args))
2399 cl = Changelist()
2400 # Ensure there actually is an issue to close.
2401 cl.GetDescription()
2402 cl.CloseIssue()
2403 return 0
2404
2405
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002406def CMDdiff(parser, args):
2407 """shows differences between local tree and last upload."""
2408 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002409 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002410 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002411 if not issue:
2412 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002413 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002414 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002415
2416 # Create a new branch based on the merge-base
2417 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2418 try:
2419 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002420 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002421 if rtn != 0:
2422 return rtn
2423
2424 # Switch back to starting brand and diff against the temporary
2425 # branch containing the latest rietveld patch.
2426 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2427 finally:
2428 RunGit(['checkout', '-q', branch])
2429 RunGit(['branch', '-D', TMP_BRANCH])
2430
2431 return 0
2432
2433
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002434def CMDowners(parser, args):
2435 """interactively find the owners for reviewing"""
2436 parser.add_option(
2437 '--no-color',
2438 action='store_true',
2439 help='Use this option to disable color output')
2440 options, args = parser.parse_args(args)
2441
2442 author = RunGit(['config', 'user.email']).strip() or None
2443
2444 cl = Changelist()
2445
2446 if args:
2447 if len(args) > 1:
2448 parser.error('Unknown args')
2449 base_branch = args[0]
2450 else:
2451 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002452 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002453
2454 change = cl.GetChange(base_branch, None)
2455 return owners_finder.OwnersFinder(
2456 [f.LocalPath() for f in
2457 cl.GetChange(base_branch, None).AffectedFiles()],
2458 change.RepositoryRoot(), author,
2459 fopen=file, os_path=os.path, glob=glob.glob,
2460 disable_color=options.no_color).run()
2461
2462
enne@chromium.org555cfe42014-01-29 18:21:39 +00002463@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002464def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002465 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002466 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002467 parser.add_option('--full', action='store_true',
2468 help='Reformat the full content of all touched files')
2469 parser.add_option('--dry-run', action='store_true',
2470 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002471 parser.add_option('--diff', action='store_true',
2472 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002473 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002474
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002475 # git diff generates paths against the root of the repository. Change
2476 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002477 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002478 if rel_base_path:
2479 os.chdir(rel_base_path)
2480
digit@chromium.org29e47272013-05-17 17:01:46 +00002481 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002482 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002483 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002484 # Only list the names of modified files.
2485 diff_cmd.append('--name-only')
2486 else:
2487 # Only generate context-less patches.
2488 diff_cmd.append('-U0')
2489
2490 # Grab the merge-base commit, i.e. the upstream commit of the current
2491 # branch when it was created or the last time it was rebased. This is
2492 # to cover the case where the user may have called "git fetch origin",
2493 # moving the origin branch to a newer commit, but hasn't rebased yet.
2494 upstream_commit = None
2495 cl = Changelist()
2496 upstream_branch = cl.GetUpstreamBranch()
2497 if upstream_branch:
2498 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2499 upstream_commit = upstream_commit.strip()
2500
2501 if not upstream_commit:
2502 DieWithError('Could not find base commit for this branch. '
2503 'Are you in detached state?')
2504
2505 diff_cmd.append(upstream_commit)
2506
2507 # Handle source file filtering.
2508 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002509 if args:
2510 for arg in args:
2511 if os.path.isdir(arg):
2512 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2513 elif os.path.isfile(arg):
2514 diff_cmd.append(arg)
2515 else:
2516 DieWithError('Argument "%s" is not a file or a directory' % arg)
2517 else:
2518 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002519 diff_output = RunGit(diff_cmd)
2520
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002521 top_dir = os.path.normpath(
2522 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2523
2524 # Locate the clang-format binary in the checkout
2525 try:
2526 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2527 except clang_format.NotFoundError, e:
2528 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002529
digit@chromium.org29e47272013-05-17 17:01:46 +00002530 if opts.full:
2531 # diff_output is a list of files to send to clang-format.
2532 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002533 if not files:
2534 print "Nothing to format."
2535 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002536 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002537 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002538 cmd.append('-i')
2539 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002540 if opts.diff:
2541 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002542 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002543 env = os.environ.copy()
2544 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002545 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002546 try:
2547 script = clang_format.FindClangFormatScriptInChromiumTree(
2548 'clang-format-diff.py')
2549 except clang_format.NotFoundError, e:
2550 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002551
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002552 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002553 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002554 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002555
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002556 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002557 if opts.diff:
2558 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002559 if opts.dry_run and len(stdout) > 0:
2560 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002561
2562 return 0
2563
2564
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002565class OptionParser(optparse.OptionParser):
2566 """Creates the option parse and add --verbose support."""
2567 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002568 optparse.OptionParser.__init__(
2569 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002570 self.add_option(
2571 '-v', '--verbose', action='count', default=0,
2572 help='Use 2 times for more debugging info')
2573
2574 def parse_args(self, args=None, values=None):
2575 options, args = optparse.OptionParser.parse_args(self, args, values)
2576 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2577 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2578 return options, args
2579
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002581def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002582 if sys.hexversion < 0x02060000:
2583 print >> sys.stderr, (
2584 '\nYour python version %s is unsupported, please upgrade.\n' %
2585 sys.version.split(' ', 1)[0])
2586 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002587
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002588 # Reload settings.
2589 global settings
2590 settings = Settings()
2591
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002592 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002593 dispatcher = subcommand.CommandDispatcher(__name__)
2594 try:
2595 return dispatcher.execute(OptionParser(), argv)
2596 except urllib2.HTTPError, e:
2597 if e.code != 500:
2598 raise
2599 DieWithError(
2600 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2601 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002602
2603
2604if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002605 # These affect sys.stdout so do it outside of main() to simplify mocks in
2606 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002607 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002608 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002609 sys.exit(main(sys.argv[1:]))