blob: 60c41f4a87b3d38a94fb8b193477a076ef3a6bd2 [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
maruel@chromium.org967c0a82013-06-17 22:52:24 +000010import difflib
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024
25try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000026 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027except ImportError:
28 pass
29
maruel@chromium.org2a74d372011-03-29 19:05:50 +000030
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000031from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032from third_party import upload
33import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000034import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000035import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000037import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000039import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import watchlists
41
42
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000043DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000044POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000045DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000046GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000047CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000049# Shortcut since it quickly becomes redundant.
50Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000051
maruel@chromium.orgddd59412011-11-30 14:20:38 +000052# Initialized in main()
53settings = None
54
55
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000057 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 sys.exit(1)
59
60
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000063 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000064 except subprocess2.CalledProcessError as e:
65 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000068 'Command "%s" failed.\n%s' % (
69 ' '.join(args), error_message or e.stdout or ''))
70 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071
72
73def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000074 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000075 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
77
78def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000080 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000081 env = os.environ.copy()
82 # 'cat' is a magical git string that disables pagers on all platforms.
83 env['GIT_PAGER'] = 'cat'
84 out, code = subprocess2.communicate(['git'] + args,
85 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000086 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000087 return code, out[0]
88 except ValueError:
89 # When the subprocess fails, it returns None. That triggers a ValueError
90 # when trying to unpack the return value into (out, code).
91 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000092
93
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000094def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000095 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000096 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000097 return (version.startswith(prefix) and
98 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000099
100
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101def usage(more):
102 def hook(fn):
103 fn.usage_more = more
104 return fn
105 return hook
106
107
maruel@chromium.org90541732011-04-01 17:54:18 +0000108def ask_for_data(prompt):
109 try:
110 return raw_input(prompt)
111 except KeyboardInterrupt:
112 # Hide the exception.
113 sys.exit(1)
114
115
iannucci@chromium.org79540052012-10-19 23:15:26 +0000116def git_set_branch_value(key, value):
117 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000118 if not branch:
119 return
120
121 cmd = ['config']
122 if isinstance(value, int):
123 cmd.append('--int')
124 git_key = 'branch.%s.%s' % (branch, key)
125 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000126
127
128def git_get_branch_default(key, default):
129 branch = Changelist().GetBranch()
130 if branch:
131 git_key = 'branch.%s.%s' % (branch, key)
132 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
133 try:
134 return int(stdout.strip())
135 except ValueError:
136 pass
137 return default
138
139
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000140def add_git_similarity(parser):
141 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000143 help='Sets the percentage that a pair of files need to match in order to'
144 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000145 parser.add_option(
146 '--find-copies', action='store_true',
147 help='Allows git to look for copies.')
148 parser.add_option(
149 '--no-find-copies', action='store_false', dest='find_copies',
150 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000151
152 old_parser_args = parser.parse_args
153 def Parse(args):
154 options, args = old_parser_args(args)
155
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000158 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159 print('Note: Saving similarity of %d%% in git config.'
160 % options.similarity)
161 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000162
iannucci@chromium.org79540052012-10-19 23:15:26 +0000163 options.similarity = max(0, min(options.similarity, 100))
164
165 if options.find_copies is None:
166 options.find_copies = bool(
167 git_get_branch_default('git-find-copies', True))
168 else:
169 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000170
171 print('Using %d%% similarity for rename/copy detection. '
172 'Override with --similarity.' % options.similarity)
173
174 return options, args
175 parser.parse_args = Parse
176
177
ukai@chromium.org259e4682012-10-25 07:36:33 +0000178def is_dirty_git_tree(cmd):
179 # Make sure index is up-to-date before running diff-index.
180 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
181 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
182 if dirty:
183 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
184 print 'Uncommitted files: (git diff-index --name-status HEAD)'
185 print dirty[:4096]
186 if len(dirty) > 4096:
187 print '... (run "git diff-index --name-status HEAD" to see full output).'
188 return True
189 return False
190
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000191
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000192def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
193 """Return the corresponding git ref if |base_url| together with |glob_spec|
194 matches the full |url|.
195
196 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
197 """
198 fetch_suburl, as_ref = glob_spec.split(':')
199 if allow_wildcards:
200 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
201 if glob_match:
202 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
203 # "branches/{472,597,648}/src:refs/remotes/svn/*".
204 branch_re = re.escape(base_url)
205 if glob_match.group(1):
206 branch_re += '/' + re.escape(glob_match.group(1))
207 wildcard = glob_match.group(2)
208 if wildcard == '*':
209 branch_re += '([^/]*)'
210 else:
211 # Escape and replace surrounding braces with parentheses and commas
212 # with pipe symbols.
213 wildcard = re.escape(wildcard)
214 wildcard = re.sub('^\\\\{', '(', wildcard)
215 wildcard = re.sub('\\\\,', '|', wildcard)
216 wildcard = re.sub('\\\\}$', ')', wildcard)
217 branch_re += wildcard
218 if glob_match.group(3):
219 branch_re += re.escape(glob_match.group(3))
220 match = re.match(branch_re, url)
221 if match:
222 return re.sub('\*$', match.group(1), as_ref)
223
224 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
225 if fetch_suburl:
226 full_url = base_url + '/' + fetch_suburl
227 else:
228 full_url = base_url
229 if full_url == url:
230 return as_ref
231 return None
232
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000233
iannucci@chromium.org79540052012-10-19 23:15:26 +0000234def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000235 """Prints statistics about the change to the user."""
236 # --no-ext-diff is broken in some versions of Git, so try to work around
237 # this by overriding the environment (but there is still a problem if the
238 # git config key "diff.external" is used).
239 env = os.environ.copy()
240 if 'GIT_EXTERNAL_DIFF' in env:
241 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000242 # 'cat' is a magical git string that disables pagers on all platforms.
243 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000244
245 if find_copies:
246 similarity_options = ['--find-copies-harder', '-l100000',
247 '-C%s' % similarity]
248 else:
249 similarity_options = ['-M%s' % similarity]
250
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000251 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000252 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000253 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000254 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000255
256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000257class Settings(object):
258 def __init__(self):
259 self.default_server = None
260 self.cc = None
261 self.root = None
262 self.is_git_svn = None
263 self.svn_branch = None
264 self.tree_status_url = None
265 self.viewvc_url = None
266 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000267 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000268 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000269
270 def LazyUpdateIfNeeded(self):
271 """Updates the settings from a codereview.settings file, if available."""
272 if not self.updated:
273 cr_settings_file = FindCodereviewSettingsFile()
274 if cr_settings_file:
275 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000276 self.updated = True
277 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278 self.updated = True
279
280 def GetDefaultServerUrl(self, error_ok=False):
281 if not self.default_server:
282 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000283 self.default_server = gclient_utils.UpgradeToHttps(
284 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285 if error_ok:
286 return self.default_server
287 if not self.default_server:
288 error_message = ('Could not find settings file. You must configure '
289 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000290 self.default_server = gclient_utils.UpgradeToHttps(
291 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292 return self.default_server
293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000294 def GetRoot(self):
295 if not self.root:
296 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
297 return self.root
298
299 def GetIsGitSvn(self):
300 """Return true if this repo looks like it's using git-svn."""
301 if self.is_git_svn is None:
302 # If you have any "svn-remote.*" config keys, we think you're using svn.
303 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000304 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000305 return self.is_git_svn
306
307 def GetSVNBranch(self):
308 if self.svn_branch is None:
309 if not self.GetIsGitSvn():
310 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
311
312 # Try to figure out which remote branch we're based on.
313 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000314 # 1) iterate through our branch history and find the svn URL.
315 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316
317 # regexp matching the git-svn line that contains the URL.
318 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
319
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000320 env = os.environ.copy()
321 # 'cat' is a magical git string that disables pagers on all platforms.
322 env['GIT_PAGER'] = 'cat'
323
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000324 # We don't want to go through all of history, so read a line from the
325 # pipe at a time.
326 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000327 cmd = ['git', 'log', '-100', '--pretty=medium']
328 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000329 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000330 for line in proc.stdout:
331 match = git_svn_re.match(line)
332 if match:
333 url = match.group(1)
334 proc.stdout.close() # Cut pipe.
335 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000336
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000337 if url:
338 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
339 remotes = RunGit(['config', '--get-regexp',
340 r'^svn-remote\..*\.url']).splitlines()
341 for remote in remotes:
342 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000343 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000344 remote = match.group(1)
345 base_url = match.group(2)
346 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000347 ['config', 'svn-remote.%s.fetch' % remote],
348 error_ok=True).strip()
349 if fetch_spec:
350 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
351 if self.svn_branch:
352 break
353 branch_spec = RunGit(
354 ['config', 'svn-remote.%s.branches' % remote],
355 error_ok=True).strip()
356 if branch_spec:
357 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
358 if self.svn_branch:
359 break
360 tag_spec = RunGit(
361 ['config', 'svn-remote.%s.tags' % remote],
362 error_ok=True).strip()
363 if tag_spec:
364 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
365 if self.svn_branch:
366 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000367
368 if not self.svn_branch:
369 DieWithError('Can\'t guess svn branch -- try specifying it on the '
370 'command line')
371
372 return self.svn_branch
373
374 def GetTreeStatusUrl(self, error_ok=False):
375 if not self.tree_status_url:
376 error_message = ('You must configure your tree status URL by running '
377 '"git cl config".')
378 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
379 error_ok=error_ok,
380 error_message=error_message)
381 return self.tree_status_url
382
383 def GetViewVCUrl(self):
384 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000385 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000386 return self.viewvc_url
387
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000388 def GetDefaultCCList(self):
389 return self._GetConfig('rietveld.cc', error_ok=True)
390
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000391 def GetDefaultPrivateFlag(self):
392 return self._GetConfig('rietveld.private', error_ok=True)
393
ukai@chromium.orge8077812012-02-03 03:41:46 +0000394 def GetIsGerrit(self):
395 """Return true if this repo is assosiated with gerrit code review system."""
396 if self.is_gerrit is None:
397 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
398 return self.is_gerrit
399
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000400 def GetGitEditor(self):
401 """Return the editor specified in the git config, or None if none is."""
402 if self.git_editor is None:
403 self.git_editor = self._GetConfig('core.editor', error_ok=True)
404 return self.git_editor or None
405
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000406 def _GetConfig(self, param, **kwargs):
407 self.LazyUpdateIfNeeded()
408 return RunGit(['config', param], **kwargs).strip()
409
410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000411def ShortBranchName(branch):
412 """Convert a name like 'refs/heads/foo' to just 'foo'."""
413 return branch.replace('refs/heads/', '')
414
415
416class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000417 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000418 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000419 global settings
420 if not settings:
421 # Happens when git_cl.py is used as a utility library.
422 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000423 settings.GetDefaultServerUrl()
424 self.branchref = branchref
425 if self.branchref:
426 self.branch = ShortBranchName(self.branchref)
427 else:
428 self.branch = None
429 self.rietveld_server = None
430 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000431 self.lookedup_issue = False
432 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433 self.has_description = False
434 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000435 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000437 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000438 self.cc = None
439 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000440 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000441 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000442
443 def GetCCList(self):
444 """Return the users cc'd on this CL.
445
446 Return is a string suitable for passing to gcl with the --cc flag.
447 """
448 if self.cc is None:
449 base_cc = settings .GetDefaultCCList()
450 more_cc = ','.join(self.watchers)
451 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
452 return self.cc
453
454 def SetWatchers(self, watchers):
455 """Set the list of email addresses that should be cc'd based on the changed
456 files in this CL.
457 """
458 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459
460 def GetBranch(self):
461 """Returns the short branch name, e.g. 'master'."""
462 if not self.branch:
463 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
464 self.branch = ShortBranchName(self.branchref)
465 return self.branch
466
467 def GetBranchRef(self):
468 """Returns the full branch name, e.g. 'refs/heads/master'."""
469 self.GetBranch() # Poke the lazy loader.
470 return self.branchref
471
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000472 @staticmethod
473 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000474 """Returns a tuple containg remote and remote ref,
475 e.g. 'origin', 'refs/heads/master'
476 """
477 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
479 error_ok=True).strip()
480 if upstream_branch:
481 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
482 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000483 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
484 error_ok=True).strip()
485 if upstream_branch:
486 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000487 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000488 # Fall back on trying a git-svn upstream branch.
489 if settings.GetIsGitSvn():
490 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000492 # Else, try to guess the origin remote.
493 remote_branches = RunGit(['branch', '-r']).split()
494 if 'origin/master' in remote_branches:
495 # Fall back on origin/master if it exits.
496 remote = 'origin'
497 upstream_branch = 'refs/heads/master'
498 elif 'origin/trunk' in remote_branches:
499 # Fall back on origin/trunk if it exists. Generally a shared
500 # git-svn clone
501 remote = 'origin'
502 upstream_branch = 'refs/heads/trunk'
503 else:
504 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000505Either pass complete "git diff"-style arguments, like
506 git cl upload origin/master
507or verify this branch is set up to track another (via the --track argument to
508"git checkout -b ...").""")
509
510 return remote, upstream_branch
511
512 def GetUpstreamBranch(self):
513 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000514 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000515 if remote is not '.':
516 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
517 self.upstream_branch = upstream_branch
518 return self.upstream_branch
519
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000520 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000521 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000522 remote, branch = None, self.GetBranch()
523 seen_branches = set()
524 while branch not in seen_branches:
525 seen_branches.add(branch)
526 remote, branch = self.FetchUpstreamTuple(branch)
527 branch = ShortBranchName(branch)
528 if remote != '.' or branch.startswith('refs/remotes'):
529 break
530 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000531 remotes = RunGit(['remote'], error_ok=True).split()
532 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000533 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000534 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000535 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000536 logging.warning('Could not determine which remote this change is '
537 'associated with, so defaulting to "%s". This may '
538 'not be what you want. You may prevent this message '
539 'by running "git svn info" as documented here: %s',
540 self._remote,
541 GIT_INSTRUCTIONS_URL)
542 else:
543 logging.warn('Could not determine which remote this change is '
544 'associated with. You may prevent this message by '
545 'running "git svn info" as documented here: %s',
546 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000547 branch = 'HEAD'
548 if branch.startswith('refs/remotes'):
549 self._remote = (remote, branch)
550 else:
551 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000552 return self._remote
553
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000554 def GitSanityChecks(self, upstream_git_obj):
555 """Checks git repo status and ensures diff is from local commits."""
556
557 # Verify the commit we're diffing against is in our current branch.
558 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
559 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
560 if upstream_sha != common_ancestor:
561 print >> sys.stderr, (
562 'ERROR: %s is not in the current branch. You may need to rebase '
563 'your tracking branch' % upstream_sha)
564 return False
565
566 # List the commits inside the diff, and verify they are all local.
567 commits_in_diff = RunGit(
568 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
569 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
570 remote_branch = remote_branch.strip()
571 if code != 0:
572 _, remote_branch = self.GetRemoteBranch()
573
574 commits_in_remote = RunGit(
575 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
576
577 common_commits = set(commits_in_diff) & set(commits_in_remote)
578 if common_commits:
579 print >> sys.stderr, (
580 'ERROR: Your diff contains %d commits already in %s.\n'
581 'Run "git log --oneline %s..HEAD" to get a list of commits in '
582 'the diff. If you are using a custom git flow, you can override'
583 ' the reference used for this check with "git config '
584 'gitcl.remotebranch <git-ref>".' % (
585 len(common_commits), remote_branch, upstream_git_obj))
586 return False
587 return True
588
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000589 def GetGitBaseUrlFromConfig(self):
590 """Return the configured base URL from branch.<branchname>.baseurl.
591
592 Returns None if it is not set.
593 """
594 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
595 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000596
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597 def GetRemoteUrl(self):
598 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
599
600 Returns None if there is no remote.
601 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000602 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
604
605 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000606 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000607 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000609 self.issue = int(issue) or None if issue else None
610 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 return self.issue
612
613 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000614 if not self.rietveld_server:
615 # If we're on a branch then get the server potentially associated
616 # with that branch.
617 if self.GetIssue():
618 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
619 ['config', self._RietveldServer()], error_ok=True).strip())
620 if not self.rietveld_server:
621 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.rietveld_server
623
624 def GetIssueURL(self):
625 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000626 if not self.GetIssue():
627 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
629
630 def GetDescription(self, pretty=False):
631 if not self.has_description:
632 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000633 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000634 try:
635 self.description = self.RpcServer().get_description(issue).strip()
636 except urllib2.HTTPError, e:
637 if e.code == 404:
638 DieWithError(
639 ('\nWhile fetching the description for issue %d, received a '
640 '404 (not found)\n'
641 'error. It is likely that you deleted this '
642 'issue on the server. If this is the\n'
643 'case, please run\n\n'
644 ' git cl issue 0\n\n'
645 'to clear the association with the deleted issue. Then run '
646 'this command again.') % issue)
647 else:
648 DieWithError(
649 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 self.has_description = True
651 if pretty:
652 wrapper = textwrap.TextWrapper()
653 wrapper.initial_indent = wrapper.subsequent_indent = ' '
654 return wrapper.fill(self.description)
655 return self.description
656
657 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000658 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000659 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 patchset = RunGit(['config', self._PatchsetSetting()],
661 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000662 self.patchset = int(patchset) or None if patchset else None
663 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000664 return self.patchset
665
666 def SetPatchset(self, patchset):
667 """Set this branch's patchset. If patchset=0, clears the patchset."""
668 if patchset:
669 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000670 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671 else:
672 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000673 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000674 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000676 def GetMostRecentPatchset(self):
677 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000678
679 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000680 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000681 '/download/issue%s_%s.diff' % (issue, patchset))
682
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000683 def GetIssueProperties(self):
684 if self._props is None:
685 issue = self.GetIssue()
686 if not issue:
687 self._props = {}
688 else:
689 self._props = self.RpcServer().get_issue_properties(issue, True)
690 return self._props
691
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000692 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000693 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000694
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 def SetIssue(self, issue):
696 """Set this branch's issue. If issue=0, clears the issue."""
697 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000698 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000699 RunGit(['config', self._IssueSetting(), str(issue)])
700 if self.rietveld_server:
701 RunGit(['config', self._RietveldServer(), self.rietveld_server])
702 else:
703 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000704 self.issue = None
705 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000707 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000708 if not self.GitSanityChecks(upstream_branch):
709 DieWithError('\nGit sanity check failure')
710
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000711 env = os.environ.copy()
712 # 'cat' is a magical git string that disables pagers on all platforms.
713 env['GIT_PAGER'] = 'cat'
714
715 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000716 if not root:
717 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000718 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000719
720 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000721 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000722 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000723 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000724 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000725 except subprocess2.CalledProcessError:
726 DieWithError(
727 ('\nFailed to diff against upstream branch %s!\n\n'
728 'This branch probably doesn\'t exist anymore. To reset the\n'
729 'tracking branch, please run\n'
730 ' git branch --set-upstream %s trunk\n'
731 'replacing trunk with origin/master or the relevant branch') %
732 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000733
maruel@chromium.org52424302012-08-29 15:14:30 +0000734 issue = self.GetIssue()
735 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000736 if issue:
737 description = self.GetDescription()
738 else:
739 # If the change was never uploaded, use the log messages of all commits
740 # up to the branch point, as git cl upload will prefill the description
741 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000742 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000743 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000744 '%s...' % (upstream_branch)],
745 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000746
747 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000748 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000749 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000750 name,
751 description,
752 absroot,
753 files,
754 issue,
755 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000756 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000757
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000758 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000759 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000760
761 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000762 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000763 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000764 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000765 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000766 except presubmit_support.PresubmitFailure, e:
767 DieWithError(
768 ('%s\nMaybe your depot_tools is out of date?\n'
769 'If all fails, contact maruel@') % e)
770
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000771 def UpdateDescription(self, description):
772 self.description = description
773 return self.RpcServer().update_description(
774 self.GetIssue(), self.description)
775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000777 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000778 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000780 def SetFlag(self, flag, value):
781 """Patchset must match."""
782 if not self.GetPatchset():
783 DieWithError('The patchset needs to match. Send another patchset.')
784 try:
785 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000786 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000787 except urllib2.HTTPError, e:
788 if e.code == 404:
789 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
790 if e.code == 403:
791 DieWithError(
792 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
793 'match?') % (self.GetIssue(), self.GetPatchset()))
794 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000796 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 """Returns an upload.RpcServer() to access this review's rietveld instance.
798 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000799 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000800 self._rpc_server = rietveld.CachingRietveld(
801 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000802 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803
804 def _IssueSetting(self):
805 """Return the git setting that stores this change's issue."""
806 return 'branch.%s.rietveldissue' % self.GetBranch()
807
808 def _PatchsetSetting(self):
809 """Return the git setting that stores this change's most recent patchset."""
810 return 'branch.%s.rietveldpatchset' % self.GetBranch()
811
812 def _RietveldServer(self):
813 """Returns the git setting that stores this change's rietveld server."""
814 return 'branch.%s.rietveldserver' % self.GetBranch()
815
816
817def GetCodereviewSettingsInteractively():
818 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000819 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000820 server = settings.GetDefaultServerUrl(error_ok=True)
821 prompt = 'Rietveld server (host[:port])'
822 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000823 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 if not server and not newserver:
825 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000826 if newserver:
827 newserver = gclient_utils.UpgradeToHttps(newserver)
828 if newserver != server:
829 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000831 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 prompt = caption
833 if initial:
834 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000835 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 if new_val == 'x':
837 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000838 elif new_val:
839 if is_url:
840 new_val = gclient_utils.UpgradeToHttps(new_val)
841 if new_val != initial:
842 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000844 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000845 SetProperty(settings.GetDefaultPrivateFlag(),
846 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000848 'tree-status-url', False)
849 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850
851 # TODO: configure a default branch to diff against, rather than this
852 # svn-based hackery.
853
854
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000855class ChangeDescription(object):
856 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000857 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000858
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000859 def __init__(self, description):
860 self._description = (description or '').strip()
861
862 @property
863 def description(self):
864 return self._description
865
866 def update_reviewers(self, reviewers):
867 """Rewrites the R=/TBR= line(s) as a single line."""
868 assert isinstance(reviewers, list), reviewers
869 if not reviewers:
870 return
871 regexp = re.compile(self.R_LINE, re.MULTILINE)
872 matches = list(regexp.finditer(self._description))
873 is_tbr = any(m.group(1) == 'TBR' for m in matches)
874 if len(matches) > 1:
875 # Erase all except the first one.
876 for i in xrange(len(matches) - 1, 0, -1):
877 self._description = (
878 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000879 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880
881 if is_tbr:
882 new_r_line = 'TBR=' + ', '.join(reviewers)
883 else:
884 new_r_line = 'R=' + ', '.join(reviewers)
885
886 if matches:
887 self._description = (
888 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000889 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000890 else:
891 self.append_footer(new_r_line)
892
893 def prompt(self):
894 """Asks the user to update the description."""
895 self._description = (
896 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000897 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000898 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000899 '#--------------------This line is 72 characters long'
900 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000901 ) + self._description
902
903 if '\nBUG=' not in self._description:
904 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000905 content = gclient_utils.RunEditor(self._description, True,
906 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000907 if not content:
908 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000909
910 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000911 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000912 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000913 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000914 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000915
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000916 def append_footer(self, line):
917 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
918 if self._description:
919 if '\n' not in self._description:
920 self._description += '\n'
921 else:
922 last_line = self._description.rsplit('\n', 1)[1]
923 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
924 not presubmit_support.Change.TAG_LINE_RE.match(line)):
925 self._description += '\n'
926 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000927
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000928 def get_reviewers(self):
929 """Retrieves the list of reviewers."""
930 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000931 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000932 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000933
934
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000935def get_approving_reviewers(props):
936 """Retrieves the reviewers that approved a CL from the issue properties with
937 messages.
938
939 Note that the list may contain reviewers that are not committer, thus are not
940 considered by the CQ.
941 """
942 return sorted(
943 set(
944 message['sender']
945 for message in props['messages']
946 if message['approval'] and message['sender'] in props['reviewers']
947 )
948 )
949
950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951def FindCodereviewSettingsFile(filename='codereview.settings'):
952 """Finds the given file starting in the cwd and going up.
953
954 Only looks up to the top of the repository unless an
955 'inherit-review-settings-ok' file exists in the root of the repository.
956 """
957 inherit_ok_file = 'inherit-review-settings-ok'
958 cwd = os.getcwd()
959 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
960 if os.path.isfile(os.path.join(root, inherit_ok_file)):
961 root = '/'
962 while True:
963 if filename in os.listdir(cwd):
964 if os.path.isfile(os.path.join(cwd, filename)):
965 return open(os.path.join(cwd, filename))
966 if cwd == root:
967 break
968 cwd = os.path.dirname(cwd)
969
970
971def LoadCodereviewSettingsFromFile(fileobj):
972 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000973 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975 def SetProperty(name, setting, unset_error_ok=False):
976 fullname = 'rietveld.' + name
977 if setting in keyvals:
978 RunGit(['config', fullname, keyvals[setting]])
979 else:
980 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
981
982 SetProperty('server', 'CODE_REVIEW_SERVER')
983 # Only server setting is required. Other settings can be absent.
984 # In that case, we ignore errors raised during option deletion attempt.
985 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000986 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
988 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
989
ukai@chromium.orge8077812012-02-03 03:41:46 +0000990 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
991 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
992 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000994 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
995 #should be of the form
996 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
997 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
998 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
999 keyvals['ORIGIN_URL_CONFIG']])
1000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001002def urlretrieve(source, destination):
1003 """urllib is broken for SSL connections via a proxy therefore we
1004 can't use urllib.urlretrieve()."""
1005 with open(destination, 'w') as f:
1006 f.write(urllib2.urlopen(source).read())
1007
1008
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001009def DownloadHooks(force):
1010 """downloads hooks
1011
1012 Args:
1013 force: True to update hooks. False to install hooks if not present.
1014 """
1015 if not settings.GetIsGerrit():
1016 return
1017 server_url = settings.GetDefaultServerUrl()
1018 src = '%s/tools/hooks/commit-msg' % server_url
1019 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1020 if not os.access(dst, os.X_OK):
1021 if os.path.exists(dst):
1022 if not force:
1023 return
1024 os.remove(dst)
1025 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001026 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001027 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1028 except Exception:
1029 if os.path.exists(dst):
1030 os.remove(dst)
1031 DieWithError('\nFailed to download hooks from %s' % src)
1032
1033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034@usage('[repo root containing codereview.settings]')
1035def CMDconfig(parser, args):
1036 """edit configuration for this tree"""
1037
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001038 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 if len(args) == 0:
1040 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001041 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 return 0
1043
1044 url = args[0]
1045 if not url.endswith('codereview.settings'):
1046 url = os.path.join(url, 'codereview.settings')
1047
1048 # Load code review settings and download hooks (if available).
1049 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001050 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 return 0
1052
1053
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001054def CMDbaseurl(parser, args):
1055 """get or set base-url for this branch"""
1056 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1057 branch = ShortBranchName(branchref)
1058 _, args = parser.parse_args(args)
1059 if not args:
1060 print("Current base-url:")
1061 return RunGit(['config', 'branch.%s.base-url' % branch],
1062 error_ok=False).strip()
1063 else:
1064 print("Setting base-url to %s" % args[0])
1065 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1066 error_ok=False).strip()
1067
1068
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001069def CMDstatus(parser, args):
1070 """show status of changelists"""
1071 parser.add_option('--field',
1072 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001073 parser.add_option('-f', '--fast', action='store_true',
1074 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 (options, args) = parser.parse_args(args)
1076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001078 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001079 if options.field.startswith('desc'):
1080 print cl.GetDescription()
1081 elif options.field == 'id':
1082 issueid = cl.GetIssue()
1083 if issueid:
1084 print issueid
1085 elif options.field == 'patch':
1086 patchset = cl.GetPatchset()
1087 if patchset:
1088 print patchset
1089 elif options.field == 'url':
1090 url = cl.GetIssueURL()
1091 if url:
1092 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001093 return 0
1094
1095 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1096 if not branches:
1097 print('No local branch found.')
1098 return 0
1099
1100 changes = (Changelist(branchref=b) for b in branches.splitlines())
1101 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1102 alignment = max(5, max(len(b) for b in branches))
1103 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001104 # Adhoc thread pool to request data concurrently.
1105 output = Queue.Queue()
1106
1107 # Silence upload.py otherwise it becomes unweldly.
1108 upload.verbosity = 0
1109
1110 if not options.fast:
1111 def fetch(b):
1112 c = Changelist(branchref=b)
1113 i = c.GetIssueURL()
1114 try:
1115 props = c.GetIssueProperties()
1116 r = c.GetApprovingReviewers() if i else None
1117 if not props.get('messages'):
1118 r = None
1119 except urllib2.HTTPError:
1120 # The issue probably doesn't exist anymore.
1121 i += ' (broken)'
1122 r = None
1123 output.put((b, i, r))
1124
1125 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1126 for t in threads:
1127 t.daemon = True
1128 t.start()
1129 else:
1130 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1131 for b in branches:
1132 c = Changelist(branchref=b)
1133 output.put((b, c.GetIssue(), None))
1134
1135 tmp = {}
1136 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001137 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001138 while branch not in tmp:
1139 b, i, r = output.get()
1140 tmp[b] = (i, r)
1141 issue, reviewers = tmp.pop(branch)
1142 if not issue:
1143 color = Fore.WHITE
1144 elif reviewers:
1145 # Was approved.
1146 color = Fore.GREEN
1147 elif reviewers is None:
1148 # No message was sent.
1149 color = Fore.RED
1150 else:
1151 color = Fore.BLUE
1152 print ' %*s: %s%s%s' % (
1153 alignment, ShortBranchName(branch), color, issue, Fore.RESET)
1154
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001155 cl = Changelist()
1156 print
1157 print 'Current branch:',
1158 if not cl.GetIssue():
1159 print 'no issue assigned.'
1160 return 0
1161 print cl.GetBranch()
1162 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1163 print 'Issue description:'
1164 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 return 0
1166
1167
1168@usage('[issue_number]')
1169def CMDissue(parser, args):
1170 """Set or display the current code review issue number.
1171
1172 Pass issue number 0 to clear the current issue.
1173"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001174 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175
1176 cl = Changelist()
1177 if len(args) > 0:
1178 try:
1179 issue = int(args[0])
1180 except ValueError:
1181 DieWithError('Pass a number to set the issue or none to list it.\n'
1182 'Maybe you want to run git cl status?')
1183 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001184 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 return 0
1186
1187
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001188def CMDcomments(parser, args):
1189 """show review comments of the current changelist"""
1190 (_, args) = parser.parse_args(args)
1191 if args:
1192 parser.error('Unsupported argument: %s' % args)
1193
1194 cl = Changelist()
1195 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001196 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001197 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001198 if message['disapproval']:
1199 color = Fore.RED
1200 elif message['approval']:
1201 color = Fore.GREEN
1202 elif message['sender'] == data['owner_email']:
1203 color = Fore.MAGENTA
1204 else:
1205 color = Fore.BLUE
1206 print '\n%s%s %s%s' % (
1207 color, message['date'].split('.', 1)[0], message['sender'],
1208 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001209 if message['text'].strip():
1210 print '\n'.join(' ' + l for l in message['text'].splitlines())
1211 return 0
1212
1213
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001214def CMDdescription(parser, args):
1215 """brings up the editor for the current CL's description."""
1216 cl = Changelist()
1217 if not cl.GetIssue():
1218 DieWithError('This branch has no associated changelist.')
1219 description = ChangeDescription(cl.GetDescription())
1220 description.prompt()
1221 cl.UpdateDescription(description.description)
1222 return 0
1223
1224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225def CreateDescriptionFromLog(args):
1226 """Pulls out the commit log to use as a base for the CL description."""
1227 log_args = []
1228 if len(args) == 1 and not args[0].endswith('.'):
1229 log_args = [args[0] + '..']
1230 elif len(args) == 1 and args[0].endswith('...'):
1231 log_args = [args[0][:-1]]
1232 elif len(args) == 2:
1233 log_args = [args[0] + '..' + args[1]]
1234 else:
1235 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001236 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237
1238
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239def CMDpresubmit(parser, args):
1240 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001241 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001243 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001244 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 (options, args) = parser.parse_args(args)
1246
ukai@chromium.org259e4682012-10-25 07:36:33 +00001247 if not options.force and is_dirty_git_tree('presubmit'):
1248 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 return 1
1250
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001251 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 if args:
1253 base_branch = args[0]
1254 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001255 # Default to diffing against the common ancestor of the upstream branch.
1256 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001258 cl.RunHook(
1259 committing=not options.upload,
1260 may_prompt=False,
1261 verbose=options.verbose,
1262 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001263 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
1265
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001266def AddChangeIdToCommitMessage(options, args):
1267 """Re-commits using the current message, assumes the commit hook is in
1268 place.
1269 """
1270 log_desc = options.message or CreateDescriptionFromLog(args)
1271 git_command = ['commit', '--amend', '-m', log_desc]
1272 RunGit(git_command)
1273 new_log_desc = CreateDescriptionFromLog(args)
1274 if CHANGE_ID in new_log_desc:
1275 print 'git-cl: Added Change-Id to commit message.'
1276 else:
1277 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1278
1279
ukai@chromium.orge8077812012-02-03 03:41:46 +00001280def GerritUpload(options, args, cl):
1281 """upload the current branch to gerrit."""
1282 # We assume the remote called "origin" is the one we want.
1283 # It is probably not worthwhile to support different workflows.
1284 remote = 'origin'
1285 branch = 'master'
1286 if options.target_branch:
1287 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001289 change_desc = ChangeDescription(
1290 options.message or CreateDescriptionFromLog(args))
1291 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001292 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001294 if CHANGE_ID not in change_desc.description:
1295 AddChangeIdToCommitMessage(options, args)
1296 if options.reviewers:
1297 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298
ukai@chromium.orge8077812012-02-03 03:41:46 +00001299 receive_options = []
1300 cc = cl.GetCCList().split(',')
1301 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001302 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001303 cc = filter(None, cc)
1304 if cc:
1305 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001306 if change_desc.get_reviewers():
1307 receive_options.extend(
1308 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309
ukai@chromium.orge8077812012-02-03 03:41:46 +00001310 git_command = ['push']
1311 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001312 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001313 ' '.join(receive_options))
1314 git_command += [remote, 'HEAD:refs/for/' + branch]
1315 RunGit(git_command)
1316 # TODO(ukai): parse Change-Id: and set issue number?
1317 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001318
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319
ukai@chromium.orge8077812012-02-03 03:41:46 +00001320def RietveldUpload(options, args, cl):
1321 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1323 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324 if options.emulate_svn_auto_props:
1325 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326
1327 change_desc = None
1328
1329 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001330 if options.title:
1331 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001332 if options.message:
1333 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001334 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335 print ("This branch is associated with issue %s. "
1336 "Adding patch to that issue." % cl.GetIssue())
1337 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001338 if options.title:
1339 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001340 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001341 change_desc = ChangeDescription(message)
1342 if options.reviewers:
1343 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001344 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001345 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001346
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001347 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348 print "Description is empty; aborting."
1349 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001350
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001351 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001352 if change_desc.get_reviewers():
1353 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001354 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001355 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001356 DieWithError("Must specify reviewers to send email.")
1357 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001358 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001359 if cc:
1360 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001362 if options.private or settings.GetDefaultPrivateFlag() == "True":
1363 upload_args.append('--private')
1364
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001365 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001366 if not options.find_copies:
1367 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 # Include the upstream repo's URL in the change -- this is useful for
1370 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001371 remote_url = cl.GetGitBaseUrlFromConfig()
1372 if not remote_url:
1373 if settings.GetIsGitSvn():
1374 # URL is dependent on the current directory.
1375 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1376 if data:
1377 keys = dict(line.split(': ', 1) for line in data.splitlines()
1378 if ': ' in line)
1379 remote_url = keys.get('URL', None)
1380 else:
1381 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1382 remote_url = (cl.GetRemoteUrl() + '@'
1383 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384 if remote_url:
1385 upload_args.extend(['--base_url', remote_url])
1386
1387 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001388 upload_args = ['upload'] + upload_args + args
1389 logging.info('upload.RealMain(%s)', upload_args)
1390 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001391 except KeyboardInterrupt:
1392 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393 except:
1394 # If we got an exception after the user typed a description for their
1395 # change, back up the description before re-raising.
1396 if change_desc:
1397 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1398 print '\nGot exception while uploading -- saving description to %s\n' \
1399 % backup_path
1400 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001401 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 backup_file.close()
1403 raise
1404
1405 if not cl.GetIssue():
1406 cl.SetIssue(issue)
1407 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001408
1409 if options.use_commit_queue:
1410 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 return 0
1412
1413
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001414def cleanup_list(l):
1415 """Fixes a list so that comma separated items are put as individual items.
1416
1417 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1418 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1419 """
1420 items = sum((i.split(',') for i in l), [])
1421 stripped_items = (i.strip() for i in items)
1422 return sorted(filter(None, stripped_items))
1423
1424
ukai@chromium.orge8077812012-02-03 03:41:46 +00001425@usage('[args to "git diff"]')
1426def CMDupload(parser, args):
1427 """upload the current changelist to codereview"""
1428 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1429 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001430 parser.add_option('--bypass-watchlists', action='store_true',
1431 dest='bypass_watchlists',
1432 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001433 parser.add_option('-f', action='store_true', dest='force',
1434 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001435 parser.add_option('-m', dest='message', help='message for patchset')
1436 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001437 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001438 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001439 help='reviewer email addresses')
1440 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001441 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001442 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001443 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001444 help='send email to reviewer immediately')
1445 parser.add_option("--emulate_svn_auto_props", action="store_true",
1446 dest="emulate_svn_auto_props",
1447 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001448 parser.add_option('-c', '--use-commit-queue', action='store_true',
1449 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001450 parser.add_option('--private', action='store_true',
1451 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001452 parser.add_option('--target_branch',
1453 help='When uploading to gerrit, remote branch to '
1454 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001455 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001456 (options, args) = parser.parse_args(args)
1457
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001458 if options.target_branch and not settings.GetIsGerrit():
1459 parser.error('Use --target_branch for non gerrit repository.')
1460
ukai@chromium.org259e4682012-10-25 07:36:33 +00001461 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001462 return 1
1463
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001464 options.reviewers = cleanup_list(options.reviewers)
1465 options.cc = cleanup_list(options.cc)
1466
ukai@chromium.orge8077812012-02-03 03:41:46 +00001467 cl = Changelist()
1468 if args:
1469 # TODO(ukai): is it ok for gerrit case?
1470 base_branch = args[0]
1471 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001472 # Default to diffing against common ancestor of upstream branch
1473 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001474 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001475
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001476 # Apply watchlists on upload.
1477 change = cl.GetChange(base_branch, None)
1478 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1479 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001480 if not options.bypass_watchlists:
1481 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001482
ukai@chromium.orge8077812012-02-03 03:41:46 +00001483 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001484 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001485 may_prompt=not options.force,
1486 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001487 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001488 if not hook_results.should_continue():
1489 return 1
1490 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001491 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001492
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001493 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001494 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001495 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001496 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001497 print ('The last upload made from this repository was patchset #%d but '
1498 'the most recent patchset on the server is #%d.'
1499 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001500 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1501 'from another machine or branch the patch you\'re uploading now '
1502 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001503 ask_for_data('About to upload; enter to confirm.')
1504
iannucci@chromium.org79540052012-10-19 23:15:26 +00001505 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001506 if settings.GetIsGerrit():
1507 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001508 ret = RietveldUpload(options, args, cl)
1509 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001510 git_set_branch_value('last-upload-hash',
1511 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001512
1513 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001514
1515
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001516def IsSubmoduleMergeCommit(ref):
1517 # When submodules are added to the repo, we expect there to be a single
1518 # non-git-svn merge commit at remote HEAD with a signature comment.
1519 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001520 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001521 return RunGit(cmd) != ''
1522
1523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001524def SendUpstream(parser, args, cmd):
1525 """Common code for CmdPush and CmdDCommit
1526
1527 Squashed commit into a single.
1528 Updates changelog with metadata (e.g. pointer to review).
1529 Pushes/dcommits the code upstream.
1530 Updates review and closes.
1531 """
1532 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1533 help='bypass upload presubmit hook')
1534 parser.add_option('-m', dest='message',
1535 help="override review description")
1536 parser.add_option('-f', action='store_true', dest='force',
1537 help="force yes to questions (don't prompt)")
1538 parser.add_option('-c', dest='contributor',
1539 help="external contributor for patch (appended to " +
1540 "description and used as author for git). Should be " +
1541 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001542 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543 (options, args) = parser.parse_args(args)
1544 cl = Changelist()
1545
1546 if not args or cmd == 'push':
1547 # Default to merging against our best guess of the upstream branch.
1548 args = [cl.GetUpstreamBranch()]
1549
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001550 if options.contributor:
1551 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1552 print "Please provide contibutor as 'First Last <email@example.com>'"
1553 return 1
1554
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001555 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001556 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557
ukai@chromium.org259e4682012-10-25 07:36:33 +00001558 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559 return 1
1560
1561 # This rev-list syntax means "show all commits not in my branch that
1562 # are in base_branch".
1563 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1564 base_branch]).splitlines()
1565 if upstream_commits:
1566 print ('Base branch "%s" has %d commits '
1567 'not in this branch.' % (base_branch, len(upstream_commits)))
1568 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1569 return 1
1570
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001571 # This is the revision `svn dcommit` will commit on top of.
1572 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1573 '--pretty=format:%H'])
1574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001576 # If the base_head is a submodule merge commit, the first parent of the
1577 # base_head should be a git-svn commit, which is what we're interested in.
1578 base_svn_head = base_branch
1579 if base_has_submodules:
1580 base_svn_head += '^1'
1581
1582 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583 if extra_commits:
1584 print ('This branch has %d additional commits not upstreamed yet.'
1585 % len(extra_commits.splitlines()))
1586 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1587 'before attempting to %s.' % (base_branch, cmd))
1588 return 1
1589
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001590 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001591 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001592 author = None
1593 if options.contributor:
1594 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001595 hook_results = cl.RunHook(
1596 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001597 may_prompt=not options.force,
1598 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001599 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001600 if not hook_results.should_continue():
1601 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001602
1603 if cmd == 'dcommit':
1604 # Check the tree status if the tree status URL is set.
1605 status = GetTreeStatus()
1606 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001607 print('The tree is closed. Please wait for it to reopen. Use '
1608 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 return 1
1610 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001611 print('Unable to determine tree status. Please verify manually and '
1612 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001613 else:
1614 breakpad.SendStack(
1615 'GitClHooksBypassedCommit',
1616 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001617 (cl.GetRietveldServer(), cl.GetIssue()),
1618 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001620 change_desc = ChangeDescription(options.message)
1621 if not change_desc.description and cl.GetIssue():
1622 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001624 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001625 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001626 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001627 else:
1628 print 'No description set.'
1629 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1630 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001631
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001632 # Keep a separate copy for the commit message, because the commit message
1633 # contains the link to the Rietveld issue, while the Rietveld message contains
1634 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001635 # Keep a separate copy for the commit message.
1636 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001637 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001638
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001639 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001640 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001641 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001643 commit_desc.append_footer('Patch from %s.' % options.contributor)
1644
1645 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646
1647 branches = [base_branch, cl.GetBranchRef()]
1648 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001649 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001650 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001652 # We want to squash all this branch's commits into one commit with the proper
1653 # description. We do this by doing a "reset --soft" to the base branch (which
1654 # keeps the working copy the same), then dcommitting that. If origin/master
1655 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1656 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001657 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001658 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1659 # Delete the branches if they exist.
1660 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1661 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1662 result = RunGitWithCode(showref_cmd)
1663 if result[0] == 0:
1664 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001665
1666 # We might be in a directory that's present in this branch but not in the
1667 # trunk. Move up to the top of the tree so that git commands that expect a
1668 # valid CWD won't fail after we check out the merge branch.
1669 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1670 if rel_base_path:
1671 os.chdir(rel_base_path)
1672
1673 # Stuff our change into the merge branch.
1674 # We wrap in a try...finally block so if anything goes wrong,
1675 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001676 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001678 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1679 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001681 RunGit(
1682 [
1683 'commit', '--author', options.contributor,
1684 '-m', commit_desc.description,
1685 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001687 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001688 if base_has_submodules:
1689 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1690 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1691 RunGit(['checkout', CHERRY_PICK_BRANCH])
1692 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001693 if cmd == 'push':
1694 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001695 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001696 retcode, output = RunGitWithCode(
1697 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1698 logging.debug(output)
1699 else:
1700 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001701 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001702 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001703 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704 finally:
1705 # And then swap back to the original branch and clean up.
1706 RunGit(['checkout', '-q', cl.GetBranch()])
1707 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001708 if base_has_submodules:
1709 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710
1711 if cl.GetIssue():
1712 if cmd == 'dcommit' and 'Committed r' in output:
1713 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1714 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001715 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1716 for l in output.splitlines(False))
1717 match = filter(None, match)
1718 if len(match) != 1:
1719 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1720 output)
1721 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 else:
1723 return 1
1724 viewvc_url = settings.GetViewVCUrl()
1725 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001726 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001727 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001728 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001729 print ('Closing issue '
1730 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001731 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001733 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001734 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001735 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001736 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1737 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001738 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001739
1740 if retcode == 0:
1741 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1742 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001743 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001744
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745 return 0
1746
1747
1748@usage('[upstream branch to apply against]')
1749def CMDdcommit(parser, args):
1750 """commit the current changelist via git-svn"""
1751 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001752 message = """This doesn't appear to be an SVN repository.
1753If your project has a git mirror with an upstream SVN master, you probably need
1754to run 'git svn init', see your project's git mirror documentation.
1755If your project has a true writeable upstream repository, you probably want
1756to run 'git cl push' instead.
1757Choose wisely, if you get this wrong, your commit might appear to succeed but
1758will instead be silently ignored."""
1759 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001760 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001761 return SendUpstream(parser, args, 'dcommit')
1762
1763
1764@usage('[upstream branch to apply against]')
1765def CMDpush(parser, args):
1766 """commit the current changelist via git"""
1767 if settings.GetIsGitSvn():
1768 print('This appears to be an SVN repository.')
1769 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001770 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771 return SendUpstream(parser, args, 'push')
1772
1773
1774@usage('<patch url or issue id>')
1775def CMDpatch(parser, args):
1776 """patch in a code review"""
1777 parser.add_option('-b', dest='newbranch',
1778 help='create a new branch off trunk for the patch')
1779 parser.add_option('-f', action='store_true', dest='force',
1780 help='with -b, clobber any existing branch')
1781 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001782 help='failed patches spew .rej files rather than '
1783 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1785 help="don't commit after patch applies")
1786 (options, args) = parser.parse_args(args)
1787 if len(args) != 1:
1788 parser.print_help()
1789 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001790 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001792 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001793 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001794
maruel@chromium.org52424302012-08-29 15:14:30 +00001795 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001797 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001798 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001799 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001800 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001802 # Assume it's a URL to the patch. Default to https.
1803 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001804 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001805 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 DieWithError('Must pass an issue ID or full URL for '
1807 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001808 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001809 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001810 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811
1812 if options.newbranch:
1813 if options.force:
1814 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001815 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001816 RunGit(['checkout', '-b', options.newbranch,
1817 Changelist().GetUpstreamBranch()])
1818
1819 # Switch up to the top-level directory, if necessary, in preparation for
1820 # applying the patch.
1821 top = RunGit(['rev-parse', '--show-cdup']).strip()
1822 if top:
1823 os.chdir(top)
1824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001825 # Git patches have a/ at the beginning of source paths. We strip that out
1826 # with a sed script rather than the -p flag to patch so we can feed either
1827 # Git or svn-style patches into the same apply command.
1828 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001829 try:
1830 patch_data = subprocess2.check_output(
1831 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1832 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 DieWithError('Git patch mungling failed.')
1834 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001835 env = os.environ.copy()
1836 # 'cat' is a magical git string that disables pagers on all platforms.
1837 env['GIT_PAGER'] = 'cat'
1838
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839 # We use "git apply" to apply the patch instead of "patch" so that we can
1840 # pick up file adds.
1841 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001842 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 if options.reject:
1844 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001845 elif IsGitVersionAtLeast('1.7.12'):
1846 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001847 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001848 subprocess2.check_call(cmd, env=env,
1849 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001850 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 DieWithError('Failed to apply the patch')
1852
1853 # If we had an issue, commit the current state and register the issue.
1854 if not options.nocommit:
1855 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1856 cl = Changelist()
1857 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001858 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001859 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 else:
1861 print "Patch applied to index."
1862 return 0
1863
1864
1865def CMDrebase(parser, args):
1866 """rebase current branch on top of svn repo"""
1867 # Provide a wrapper for git svn rebase to help avoid accidental
1868 # git svn dcommit.
1869 # It's the only command that doesn't use parser at all since we just defer
1870 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001871 env = os.environ.copy()
1872 # 'cat' is a magical git string that disables pagers on all platforms.
1873 env['GIT_PAGER'] = 'cat'
1874
1875 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876
1877
1878def GetTreeStatus():
1879 """Fetches the tree status and returns either 'open', 'closed',
1880 'unknown' or 'unset'."""
1881 url = settings.GetTreeStatusUrl(error_ok=True)
1882 if url:
1883 status = urllib2.urlopen(url).read().lower()
1884 if status.find('closed') != -1 or status == '0':
1885 return 'closed'
1886 elif status.find('open') != -1 or status == '1':
1887 return 'open'
1888 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001889 return 'unset'
1890
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892def GetTreeStatusReason():
1893 """Fetches the tree status from a json url and returns the message
1894 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001895 url = settings.GetTreeStatusUrl()
1896 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897 connection = urllib2.urlopen(json_url)
1898 status = json.loads(connection.read())
1899 connection.close()
1900 return status['message']
1901
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001902
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903def CMDtree(parser, args):
1904 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001905 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906 status = GetTreeStatus()
1907 if 'unset' == status:
1908 print 'You must configure your tree status URL by running "git cl config".'
1909 return 2
1910
1911 print "The tree is %s" % status
1912 print
1913 print GetTreeStatusReason()
1914 if status != 'open':
1915 return 1
1916 return 0
1917
1918
maruel@chromium.org15192402012-09-06 12:38:29 +00001919def CMDtry(parser, args):
1920 """Triggers a try job through Rietveld."""
1921 group = optparse.OptionGroup(parser, "Try job options")
1922 group.add_option(
1923 "-b", "--bot", action="append",
1924 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1925 "times to specify multiple builders. ex: "
1926 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1927 "the try server waterfall for the builders name and the tests "
1928 "available. Can also be used to specify gtest_filter, e.g. "
1929 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1930 group.add_option(
1931 "-r", "--revision",
1932 help="Revision to use for the try job; default: the "
1933 "revision will be determined by the try server; see "
1934 "its waterfall for more info")
1935 group.add_option(
1936 "-c", "--clobber", action="store_true", default=False,
1937 help="Force a clobber before building; e.g. don't do an "
1938 "incremental build")
1939 group.add_option(
1940 "--project",
1941 help="Override which project to use. Projects are defined "
1942 "server-side to define what default bot set to use")
1943 group.add_option(
1944 "-t", "--testfilter", action="append", default=[],
1945 help=("Apply a testfilter to all the selected builders. Unless the "
1946 "builders configurations are similar, use multiple "
1947 "--bot <builder>:<test> arguments."))
1948 group.add_option(
1949 "-n", "--name", help="Try job name; default to current branch name")
1950 parser.add_option_group(group)
1951 options, args = parser.parse_args(args)
1952
1953 if args:
1954 parser.error('Unknown arguments: %s' % args)
1955
1956 cl = Changelist()
1957 if not cl.GetIssue():
1958 parser.error('Need to upload first')
1959
1960 if not options.name:
1961 options.name = cl.GetBranch()
1962
1963 # Process --bot and --testfilter.
1964 if not options.bot:
1965 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001966 change = cl.GetChange(
1967 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1968 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001969 options.bot = presubmit_support.DoGetTrySlaves(
1970 change,
1971 change.LocalPaths(),
1972 settings.GetRoot(),
1973 None,
1974 None,
1975 options.verbose,
1976 sys.stdout)
1977 if not options.bot:
1978 parser.error('No default try builder to try, use --bot')
1979
1980 builders_and_tests = {}
1981 for bot in options.bot:
1982 if ':' in bot:
1983 builder, tests = bot.split(':', 1)
1984 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1985 elif ',' in bot:
1986 parser.error('Specify one bot per --bot flag')
1987 else:
1988 builders_and_tests.setdefault(bot, []).append('defaulttests')
1989
1990 if options.testfilter:
1991 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1992 builders_and_tests = dict(
1993 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1994 if t != ['compile'])
1995
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001996 if any('triggered' in b for b in builders_and_tests):
1997 print >> sys.stderr, (
1998 'ERROR You are trying to send a job to a triggered bot. This type of'
1999 ' bot requires an\ninitial job from a parent (usually a builder). '
2000 'Instead send your job to the parent.\n'
2001 'Bot list: %s' % builders_and_tests)
2002 return 1
2003
maruel@chromium.org15192402012-09-06 12:38:29 +00002004 patchset = cl.GetPatchset()
2005 if not cl.GetPatchset():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002006 patchset = cl.GetMostRecentPatchset()
maruel@chromium.org15192402012-09-06 12:38:29 +00002007
2008 cl.RpcServer().trigger_try_jobs(
2009 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2010 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002011 print('Tried jobs on:')
2012 length = max(len(builder) for builder in builders_and_tests)
2013 for builder in sorted(builders_and_tests):
2014 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002015 return 0
2016
2017
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002018@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002020 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002021 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002022 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002023 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002024 return 0
2025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002027 if args:
2028 # One arg means set upstream branch.
2029 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2030 cl = Changelist()
2031 print "Upstream branch set to " + cl.GetUpstreamBranch()
2032 else:
2033 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002034 return 0
2035
2036
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002037def CMDset_commit(parser, args):
2038 """set the commit bit"""
2039 _, args = parser.parse_args(args)
2040 if args:
2041 parser.error('Unrecognized args: %s' % ' '.join(args))
2042 cl = Changelist()
2043 cl.SetFlag('commit', '1')
2044 return 0
2045
2046
groby@chromium.org411034a2013-02-26 15:12:01 +00002047def CMDset_close(parser, args):
2048 """close the issue"""
2049 _, args = parser.parse_args(args)
2050 if args:
2051 parser.error('Unrecognized args: %s' % ' '.join(args))
2052 cl = Changelist()
2053 # Ensure there actually is an issue to close.
2054 cl.GetDescription()
2055 cl.CloseIssue()
2056 return 0
2057
2058
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002059def CMDformat(parser, args):
2060 """run clang-format on the diff"""
2061 CLANG_EXTS = ['.cc', '.cpp', '.h']
2062 parser.add_option('--full', action='store_true', default=False)
2063 opts, args = parser.parse_args(args)
2064 if args:
2065 parser.error('Unrecognized args: %s' % ' '.join(args))
2066
digit@chromium.org29e47272013-05-17 17:01:46 +00002067 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002068 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002069 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002070 # Only list the names of modified files.
2071 diff_cmd.append('--name-only')
2072 else:
2073 # Only generate context-less patches.
2074 diff_cmd.append('-U0')
2075
2076 # Grab the merge-base commit, i.e. the upstream commit of the current
2077 # branch when it was created or the last time it was rebased. This is
2078 # to cover the case where the user may have called "git fetch origin",
2079 # moving the origin branch to a newer commit, but hasn't rebased yet.
2080 upstream_commit = None
2081 cl = Changelist()
2082 upstream_branch = cl.GetUpstreamBranch()
2083 if upstream_branch:
2084 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2085 upstream_commit = upstream_commit.strip()
2086
2087 if not upstream_commit:
2088 DieWithError('Could not find base commit for this branch. '
2089 'Are you in detached state?')
2090
2091 diff_cmd.append(upstream_commit)
2092
2093 # Handle source file filtering.
2094 diff_cmd.append('--')
2095 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2096 diff_output = RunGit(diff_cmd)
2097
2098 if opts.full:
2099 # diff_output is a list of files to send to clang-format.
2100 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002101 if not files:
2102 print "Nothing to format."
2103 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002104 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002105 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002106 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002107 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2108 'clang-format-diff.py')
2109 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002110 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2111 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2112 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002113
2114 return 0
2115
2116
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002117### Glue code for subcommand handling.
2118
2119
2120def Commands():
2121 """Returns a dict of command and their handling function."""
2122 module = sys.modules[__name__]
2123 cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
2124 return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
2125
2126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127def Command(name):
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002128 """Retrieves the function to handle a command."""
2129 commands = Commands()
2130 if name in commands:
2131 return commands[name]
2132
2133 # Try to be smart and look if there's something similar.
2134 commands_with_prefix = [c for c in commands if c.startswith(name)]
2135 if len(commands_with_prefix) == 1:
2136 return commands[commands_with_prefix[0]]
2137
2138 # A #closeenough approximation of levenshtein distance.
2139 def close_enough(a, b):
2140 return difflib.SequenceMatcher(a=a, b=b).ratio()
2141
2142 hamming_commands = sorted(
2143 ((close_enough(c, name), c) for c in commands),
2144 reverse=True)
2145 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
2146 # Too ambiguous.
2147 return
2148
2149 if hamming_commands[0][0] < 0.8:
2150 # Not similar enough. Don't be a fool and run a random command.
2151 return
2152
2153 return commands[hamming_commands[0][1]]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154
2155
2156def CMDhelp(parser, args):
2157 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002158 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002159 if len(args) == 1:
2160 return main(args + ['--help'])
2161 parser.print_help()
2162 return 0
2163
2164
2165def GenUsage(parser, command):
2166 """Modify an OptParse object with the function's documentation."""
2167 obj = Command(command)
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002168 # Get back the real command name in case Command() guess the actual command
2169 # name.
2170 command = obj.__name__[3:]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002171 more = getattr(obj, 'usage_more', '')
2172 if command == 'help':
2173 command = '<command>'
2174 else:
2175 # OptParser.description prefer nicely non-formatted strings.
2176 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2177 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2178
2179
2180def main(argv):
2181 """Doesn't parse the arguments here, just find the right subcommand to
2182 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002183 if sys.hexversion < 0x02060000:
2184 print >> sys.stderr, (
2185 '\nYour python version %s is unsupported, please upgrade.\n' %
2186 sys.version.split(' ', 1)[0])
2187 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002188
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002189 # Reload settings.
2190 global settings
2191 settings = Settings()
2192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002193 # Do it late so all commands are listed.
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002194 commands = Commands()
2195 length = max(len(c) for c in commands)
2196 docs = sorted(
2197 (name, handler.__doc__.split('\n')[0].strip())
2198 for name, handler in commands.iteritems())
2199 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
2200 ' %-*s %s' % (length, name, doc) for name, doc in docs))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201
2202 # Create the option parse and add --verbose support.
2203 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002204 parser.add_option(
2205 '-v', '--verbose', action='count', default=0,
2206 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002207 old_parser_args = parser.parse_args
2208 def Parse(args):
2209 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002210 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002211 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002212 elif options.verbose:
2213 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002214 else:
2215 logging.basicConfig(level=logging.WARNING)
2216 return options, args
2217 parser.parse_args = Parse
2218
2219 if argv:
2220 command = Command(argv[0])
2221 if command:
2222 # "fix" the usage and the description now that we know the subcommand.
2223 GenUsage(parser, argv[0])
2224 try:
2225 return command(parser, argv[1:])
2226 except urllib2.HTTPError, e:
2227 if e.code != 500:
2228 raise
2229 DieWithError(
2230 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2231 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2232
2233 # Not a known command. Default to help.
2234 GenUsage(parser, 'help')
2235 return CMDhelp(parser, argv)
2236
2237
2238if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002239 # These affect sys.stdout so do it outside of main() to simplify mocks in
2240 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002241 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002242 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002243 sys.exit(main(sys.argv[1:]))