blob: a360cff377712e6b7f883d8f2ec450113202df9d [file] [log] [blame]
[email protected]725f1c32011-04-01 20:24:541#!/usr/bin/env python
[email protected]183df1a2012-01-04 19:44:552# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]725f1c32011-04-01 20:24:543# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
[email protected]cc51cd02010-12-23 00:48:396# Copyright (C) 2008 Evan Martin <[email protected]>
7
[email protected]725f1c32011-04-01 20:24:548"""A git-command for integrating reviews on Rietveld."""
9
[email protected]6a0b07c2013-07-10 01:29:1910from distutils.version import LooseVersion
[email protected]faf3fdf2013-09-20 02:11:4811import glob
[email protected]4f6852c2012-04-20 20:39:2012import json
[email protected]cc51cd02010-12-23 00:48:3913import logging
14import optparse
15import os
[email protected]1033efd2013-07-23 23:25:0916import Queue
[email protected]cc51cd02010-12-23 00:48:3917import re
[email protected]78c4b982012-02-14 02:20:2618import stat
[email protected]cc51cd02010-12-23 00:48:3919import sys
[email protected]cc51cd02010-12-23 00:48:3920import textwrap
[email protected]1033efd2013-07-23 23:25:0921import threading
[email protected]cc51cd02010-12-23 00:48:3922import urllib2
[email protected]967c0a82013-06-17 22:52:2423import urlparse
[email protected]00858c82013-12-02 23:08:0324import webbrowser
[email protected]cc51cd02010-12-23 00:48:3925
26try:
[email protected]c98c0c52011-04-06 13:39:4327 import readline # pylint: disable=F0401,W0611
[email protected]cc51cd02010-12-23 00:48:3928except ImportError:
29 pass
30
[email protected]2a74d372011-03-29 19:05:5031
[email protected]2e23ce32013-05-07 12:42:2832from third_party import colorama
[email protected]2a74d372011-03-29 19:05:5033from third_party import upload
34import breakpad # pylint: disable=W0611
[email protected]6f09cd92011-04-01 16:38:1235import fix_encoding
[email protected]0e0436a2011-10-25 13:32:4136import gclient_utils
[email protected]2a74d372011-03-29 19:05:5037import presubmit_support
[email protected]cab38e92011-04-09 00:30:5138import rietveld
[email protected]2a74d372011-03-29 19:05:5039import scm
[email protected]0633fb42013-08-16 20:06:1440import subcommand
[email protected]32f9f5e2011-09-14 13:41:4741import subprocess2
[email protected]2a74d372011-03-29 19:05:5042import watchlists
[email protected]faf3fdf2013-09-20 02:11:4843import owners_finder
[email protected]2a74d372011-03-29 19:05:5044
[email protected]0633fb42013-08-16 20:06:1445__version__ = '1.0'
[email protected]2a74d372011-03-29 19:05:5046
[email protected]eb5edbc2012-01-16 17:03:2847DEFAULT_SERVER = 'https://codereview.appspot.com'
[email protected]0ba7f962011-01-11 22:13:5848POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
[email protected]cc51cd02010-12-23 00:48:3949DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
[email protected]d6617f32013-11-19 00:34:5450GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
[email protected]aebe87f2012-10-22 20:34:2151CHANGE_ID = 'Change-Id:'
[email protected]cc51cd02010-12-23 00:48:3952
[email protected]2e23ce32013-05-07 12:42:2853# Shortcut since it quickly becomes redundant.
54Fore = colorama.Fore
[email protected]90541732011-04-01 17:54:1855
[email protected]ddd59412011-11-30 14:20:3856# Initialized in main()
57settings = None
58
59
[email protected]cc51cd02010-12-23 00:48:3960def DieWithError(message):
[email protected]970c5222011-03-12 00:32:2461 print >> sys.stderr, message
[email protected]cc51cd02010-12-23 00:48:3962 sys.exit(1)
63
64
[email protected]32f9f5e2011-09-14 13:41:4765def RunCommand(args, error_ok=False, error_message=None, **kwargs):
[email protected]cc51cd02010-12-23 00:48:3966 try:
[email protected]373af802012-05-25 21:07:3367 return subprocess2.check_output(args, shell=False, **kwargs)
[email protected]78936cb2013-04-11 00:17:5268 except subprocess2.CalledProcessError as e:
69 logging.debug('Failed running %s', args)
[email protected]32f9f5e2011-09-14 13:41:4770 if not error_ok:
[email protected]cc51cd02010-12-23 00:48:3971 DieWithError(
[email protected]32f9f5e2011-09-14 13:41:4772 'Command "%s" failed.\n%s' % (
73 ' '.join(args), error_message or e.stdout or ''))
74 return e.stdout
[email protected]cc51cd02010-12-23 00:48:3975
76
77def RunGit(args, **kwargs):
[email protected]32f9f5e2011-09-14 13:41:4778 """Returns stdout."""
[email protected]82b91cd2013-07-09 06:33:4179 return RunCommand(['git'] + args, **kwargs)
[email protected]cc51cd02010-12-23 00:48:3980
81
82def RunGitWithCode(args):
[email protected]32f9f5e2011-09-14 13:41:4783 """Returns return code and stdout."""
[email protected]9bb85e22012-06-13 20:28:2384 try:
[email protected]82b91cd2013-07-09 06:33:4185 env = os.environ.copy()
86 # 'cat' is a magical git string that disables pagers on all platforms.
87 env['GIT_PAGER'] = 'cat'
88 out, code = subprocess2.communicate(['git'] + args,
89 env=env,
[email protected]f267b0e2013-05-02 09:11:4390 stdout=subprocess2.PIPE)
[email protected]9bb85e22012-06-13 20:28:2391 return code, out[0]
92 except ValueError:
93 # When the subprocess fails, it returns None. That triggers a ValueError
94 # when trying to unpack the return value into (out, code).
95 return 1, ''
[email protected]cc51cd02010-12-23 00:48:3996
97
[email protected]6a0b07c2013-07-10 01:29:1998def IsGitVersionAtLeast(min_version):
[email protected]cc56ee42013-07-10 22:16:2999 prefix = 'git version '
[email protected]6a0b07c2013-07-10 01:29:19100 version = RunGit(['--version']).strip()
[email protected]cc56ee42013-07-10 22:16:29101 return (version.startswith(prefix) and
102 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
[email protected]6a0b07c2013-07-10 01:29:19103
104
[email protected]90541732011-04-01 17:54:18105def ask_for_data(prompt):
106 try:
107 return raw_input(prompt)
108 except KeyboardInterrupt:
109 # Hide the exception.
110 sys.exit(1)
111
112
[email protected]79540052012-10-19 23:15:26113def git_set_branch_value(key, value):
114 branch = Changelist().GetBranch()
[email protected]caa16552013-03-18 20:45:05115 if not branch:
116 return
117
118 cmd = ['config']
119 if isinstance(value, int):
120 cmd.append('--int')
121 git_key = 'branch.%s.%s' % (branch, key)
122 RunGit(cmd + [git_key, str(value)])
[email protected]79540052012-10-19 23:15:26123
124
125def git_get_branch_default(key, default):
126 branch = Changelist().GetBranch()
127 if branch:
128 git_key = 'branch.%s.%s' % (branch, key)
129 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
130 try:
131 return int(stdout.strip())
132 except ValueError:
133 pass
134 return default
135
136
[email protected]53937ba2012-10-02 18:20:43137def add_git_similarity(parser):
138 parser.add_option(
[email protected]79540052012-10-19 23:15:26139 '--similarity', metavar='SIM', type='int', action='store',
[email protected]53937ba2012-10-02 18:20:43140 help='Sets the percentage that a pair of files need to match in order to'
141 ' be considered copies (default 50)')
[email protected]79540052012-10-19 23:15:26142 parser.add_option(
143 '--find-copies', action='store_true',
144 help='Allows git to look for copies.')
145 parser.add_option(
146 '--no-find-copies', action='store_false', dest='find_copies',
147 help='Disallows git from looking for copies.')
[email protected]53937ba2012-10-02 18:20:43148
149 old_parser_args = parser.parse_args
150 def Parse(args):
151 options, args = old_parser_args(args)
152
[email protected]53937ba2012-10-02 18:20:43153 if options.similarity is None:
[email protected]79540052012-10-19 23:15:26154 options.similarity = git_get_branch_default('git-cl-similarity', 50)
[email protected]53937ba2012-10-02 18:20:43155 else:
[email protected]79540052012-10-19 23:15:26156 print('Note: Saving similarity of %d%% in git config.'
157 % options.similarity)
158 git_set_branch_value('git-cl-similarity', options.similarity)
[email protected]53937ba2012-10-02 18:20:43159
[email protected]79540052012-10-19 23:15:26160 options.similarity = max(0, min(options.similarity, 100))
161
162 if options.find_copies is None:
163 options.find_copies = bool(
164 git_get_branch_default('git-find-copies', True))
165 else:
166 git_set_branch_value('git-find-copies', int(options.find_copies))
[email protected]53937ba2012-10-02 18:20:43167
168 print('Using %d%% similarity for rename/copy detection. '
169 'Override with --similarity.' % options.similarity)
170
171 return options, args
172 parser.parse_args = Parse
173
174
[email protected]259e4682012-10-25 07:36:33175def is_dirty_git_tree(cmd):
176 # Make sure index is up-to-date before running diff-index.
177 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
178 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
179 if dirty:
180 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
181 print 'Uncommitted files: (git diff-index --name-status HEAD)'
182 print dirty[:4096]
183 if len(dirty) > 4096:
184 print '... (run "git diff-index --name-status HEAD" to see full output).'
185 return True
186 return False
187
[email protected]0f58fa82012-11-05 01:45:20188
[email protected]866276c2011-03-18 20:09:31189def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
190 """Return the corresponding git ref if |base_url| together with |glob_spec|
191 matches the full |url|.
192
193 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
194 """
195 fetch_suburl, as_ref = glob_spec.split(':')
196 if allow_wildcards:
197 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
198 if glob_match:
199 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
200 # "branches/{472,597,648}/src:refs/remotes/svn/*".
201 branch_re = re.escape(base_url)
202 if glob_match.group(1):
203 branch_re += '/' + re.escape(glob_match.group(1))
204 wildcard = glob_match.group(2)
205 if wildcard == '*':
206 branch_re += '([^/]*)'
207 else:
208 # Escape and replace surrounding braces with parentheses and commas
209 # with pipe symbols.
210 wildcard = re.escape(wildcard)
211 wildcard = re.sub('^\\\\{', '(', wildcard)
212 wildcard = re.sub('\\\\,', '|', wildcard)
213 wildcard = re.sub('\\\\}$', ')', wildcard)
214 branch_re += wildcard
215 if glob_match.group(3):
216 branch_re += re.escape(glob_match.group(3))
217 match = re.match(branch_re, url)
218 if match:
219 return re.sub('\*$', match.group(1), as_ref)
220
221 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
222 if fetch_suburl:
223 full_url = base_url + '/' + fetch_suburl
224 else:
225 full_url = base_url
226 if full_url == url:
227 return as_ref
228 return None
229
[email protected]32f9f5e2011-09-14 13:41:47230
[email protected]79540052012-10-19 23:15:26231def print_stats(similarity, find_copies, args):
[email protected]49e3d802012-07-18 23:54:45232 """Prints statistics about the change to the user."""
233 # --no-ext-diff is broken in some versions of Git, so try to work around
234 # this by overriding the environment (but there is still a problem if the
235 # git config key "diff.external" is used).
236 env = os.environ.copy()
237 if 'GIT_EXTERNAL_DIFF' in env:
238 del env['GIT_EXTERNAL_DIFF']
[email protected]82b91cd2013-07-09 06:33:41239 # 'cat' is a magical git string that disables pagers on all platforms.
240 env['GIT_PAGER'] = 'cat'
[email protected]79540052012-10-19 23:15:26241
242 if find_copies:
243 similarity_options = ['--find-copies-harder', '-l100000',
244 '-C%s' % similarity]
245 else:
246 similarity_options = ['-M%s' % similarity]
247
[email protected]49e3d802012-07-18 23:54:45248 return subprocess2.call(
[email protected]82b91cd2013-07-09 06:33:41249 ['git',
[email protected]f267b0e2013-05-02 09:11:43250 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
[email protected]79540052012-10-19 23:15:26251 env=env)
[email protected]49e3d802012-07-18 23:54:45252
253
[email protected]cc51cd02010-12-23 00:48:39254class Settings(object):
255 def __init__(self):
256 self.default_server = None
257 self.cc = None
258 self.root = None
259 self.is_git_svn = None
260 self.svn_branch = None
261 self.tree_status_url = None
262 self.viewvc_url = None
263 self.updated = False
[email protected]e8077812012-02-03 03:41:46264 self.is_gerrit = None
[email protected]615a2622013-05-03 13:20:14265 self.git_editor = None
[email protected]cc51cd02010-12-23 00:48:39266
267 def LazyUpdateIfNeeded(self):
268 """Updates the settings from a codereview.settings file, if available."""
269 if not self.updated:
270 cr_settings_file = FindCodereviewSettingsFile()
271 if cr_settings_file:
272 LoadCodereviewSettingsFromFile(cr_settings_file)
[email protected]78c4b982012-02-14 02:20:26273 self.updated = True
274 DownloadHooks(False)
[email protected]cc51cd02010-12-23 00:48:39275 self.updated = True
276
277 def GetDefaultServerUrl(self, error_ok=False):
278 if not self.default_server:
279 self.LazyUpdateIfNeeded()
[email protected]eb5edbc2012-01-16 17:03:28280 self.default_server = gclient_utils.UpgradeToHttps(
281 self._GetConfig('rietveld.server', error_ok=True))
[email protected]cc51cd02010-12-23 00:48:39282 if error_ok:
283 return self.default_server
284 if not self.default_server:
285 error_message = ('Could not find settings file. You must configure '
286 'your review setup by running "git cl config".')
[email protected]eb5edbc2012-01-16 17:03:28287 self.default_server = gclient_utils.UpgradeToHttps(
288 self._GetConfig('rietveld.server', error_message=error_message))
[email protected]cc51cd02010-12-23 00:48:39289 return self.default_server
290
[email protected]cc51cd02010-12-23 00:48:39291 def GetRoot(self):
292 if not self.root:
293 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
294 return self.root
295
296 def GetIsGitSvn(self):
297 """Return true if this repo looks like it's using git-svn."""
298 if self.is_git_svn is None:
299 # If you have any "svn-remote.*" config keys, we think you're using svn.
300 self.is_git_svn = RunGitWithCode(
[email protected]3cdcf562013-04-12 19:39:38301 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
[email protected]cc51cd02010-12-23 00:48:39302 return self.is_git_svn
303
304 def GetSVNBranch(self):
305 if self.svn_branch is None:
306 if not self.GetIsGitSvn():
307 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
308
309 # Try to figure out which remote branch we're based on.
310 # Strategy:
[email protected]ade368c2011-03-01 08:57:50311 # 1) iterate through our branch history and find the svn URL.
312 # 2) find the svn-remote that fetches from the URL.
[email protected]cc51cd02010-12-23 00:48:39313
314 # regexp matching the git-svn line that contains the URL.
315 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
316
[email protected]82b91cd2013-07-09 06:33:41317 env = os.environ.copy()
318 # 'cat' is a magical git string that disables pagers on all platforms.
319 env['GIT_PAGER'] = 'cat'
320
[email protected]ade368c2011-03-01 08:57:50321 # We don't want to go through all of history, so read a line from the
322 # pipe at a time.
323 # The -100 is an arbitrary limit so we don't search forever.
[email protected]82b91cd2013-07-09 06:33:41324 cmd = ['git', 'log', '-100', '--pretty=medium']
325 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
[email protected]740f9d72011-06-10 18:33:10326 url = None
[email protected]ade368c2011-03-01 08:57:50327 for line in proc.stdout:
328 match = git_svn_re.match(line)
329 if match:
330 url = match.group(1)
331 proc.stdout.close() # Cut pipe.
332 break
[email protected]cc51cd02010-12-23 00:48:39333
[email protected]ade368c2011-03-01 08:57:50334 if url:
335 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
336 remotes = RunGit(['config', '--get-regexp',
337 r'^svn-remote\..*\.url']).splitlines()
338 for remote in remotes:
339 match = svn_remote_re.match(remote)
[email protected]cc51cd02010-12-23 00:48:39340 if match:
[email protected]ade368c2011-03-01 08:57:50341 remote = match.group(1)
342 base_url = match.group(2)
[email protected]4ac25532013-12-16 22:07:02343 rewrite_root = RunGit(
344 ['config', 'svn-remote.%s.rewriteRoot' % remote],
345 error_ok=True).strip()
346 if rewrite_root:
347 base_url = rewrite_root
[email protected]ade368c2011-03-01 08:57:50348 fetch_spec = RunGit(
[email protected]866276c2011-03-18 20:09:31349 ['config', 'svn-remote.%s.fetch' % remote],
350 error_ok=True).strip()
351 if fetch_spec:
352 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
353 if self.svn_branch:
354 break
355 branch_spec = RunGit(
356 ['config', 'svn-remote.%s.branches' % remote],
357 error_ok=True).strip()
358 if branch_spec:
359 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
360 if self.svn_branch:
361 break
362 tag_spec = RunGit(
363 ['config', 'svn-remote.%s.tags' % remote],
364 error_ok=True).strip()
365 if tag_spec:
366 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
367 if self.svn_branch:
368 break
[email protected]cc51cd02010-12-23 00:48:39369
370 if not self.svn_branch:
371 DieWithError('Can\'t guess svn branch -- try specifying it on the '
372 'command line')
373
374 return self.svn_branch
375
376 def GetTreeStatusUrl(self, error_ok=False):
377 if not self.tree_status_url:
378 error_message = ('You must configure your tree status URL by running '
379 '"git cl config".')
380 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
381 error_ok=error_ok,
382 error_message=error_message)
383 return self.tree_status_url
384
385 def GetViewVCUrl(self):
386 if not self.viewvc_url:
[email protected]a78f7c02012-11-28 02:06:45387 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
[email protected]cc51cd02010-12-23 00:48:39388 return self.viewvc_url
389
[email protected]ae6df352011-04-06 17:40:39390 def GetDefaultCCList(self):
391 return self._GetConfig('rietveld.cc', error_ok=True)
392
[email protected]c1737d02013-05-29 14:17:28393 def GetDefaultPrivateFlag(self):
394 return self._GetConfig('rietveld.private', error_ok=True)
395
[email protected]e8077812012-02-03 03:41:46396 def GetIsGerrit(self):
397 """Return true if this repo is assosiated with gerrit code review system."""
398 if self.is_gerrit is None:
399 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
400 return self.is_gerrit
401
[email protected]615a2622013-05-03 13:20:14402 def GetGitEditor(self):
403 """Return the editor specified in the git config, or None if none is."""
404 if self.git_editor is None:
405 self.git_editor = self._GetConfig('core.editor', error_ok=True)
406 return self.git_editor or None
407
[email protected]cc51cd02010-12-23 00:48:39408 def _GetConfig(self, param, **kwargs):
409 self.LazyUpdateIfNeeded()
410 return RunGit(['config', param], **kwargs).strip()
411
412
[email protected]cc51cd02010-12-23 00:48:39413def ShortBranchName(branch):
414 """Convert a name like 'refs/heads/foo' to just 'foo'."""
415 return branch.replace('refs/heads/', '')
416
417
418class Changelist(object):
[email protected]1033efd2013-07-23 23:25:09419 def __init__(self, branchref=None, issue=None):
[email protected]cc51cd02010-12-23 00:48:39420 # Poke settings so we get the "configure your server" message if necessary.
[email protected]379d07a2011-11-30 14:58:10421 global settings
422 if not settings:
423 # Happens when git_cl.py is used as a utility library.
424 settings = Settings()
[email protected]cc51cd02010-12-23 00:48:39425 settings.GetDefaultServerUrl()
426 self.branchref = branchref
427 if self.branchref:
428 self.branch = ShortBranchName(self.branchref)
429 else:
430 self.branch = None
431 self.rietveld_server = None
432 self.upstream_branch = None
[email protected]1033efd2013-07-23 23:25:09433 self.lookedup_issue = False
434 self.issue = issue or None
[email protected]cc51cd02010-12-23 00:48:39435 self.has_description = False
436 self.description = None
[email protected]1033efd2013-07-23 23:25:09437 self.lookedup_patchset = False
[email protected]cc51cd02010-12-23 00:48:39438 self.patchset = None
[email protected]e77ebbf2011-03-29 20:35:38439 self._rpc_server = None
[email protected]ae6df352011-04-06 17:40:39440 self.cc = None
441 self.watchers = ()
[email protected]a2cbbbb2012-03-22 20:40:40442 self._remote = None
[email protected]1033efd2013-07-23 23:25:09443 self._props = None
[email protected]ae6df352011-04-06 17:40:39444
445 def GetCCList(self):
446 """Return the users cc'd on this CL.
447
448 Return is a string suitable for passing to gcl with the --cc flag.
449 """
450 if self.cc is None:
[email protected]99918ab2013-09-30 06:17:28451 base_cc = settings.GetDefaultCCList()
[email protected]ae6df352011-04-06 17:40:39452 more_cc = ','.join(self.watchers)
453 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
454 return self.cc
455
[email protected]99918ab2013-09-30 06:17:28456 def GetCCListWithoutDefault(self):
457 """Return the users cc'd on this CL excluding default ones."""
458 if self.cc is None:
459 self.cc = ','.join(self.watchers)
460 return self.cc
461
[email protected]ae6df352011-04-06 17:40:39462 def SetWatchers(self, watchers):
463 """Set the list of email addresses that should be cc'd based on the changed
464 files in this CL.
465 """
466 self.watchers = watchers
[email protected]cc51cd02010-12-23 00:48:39467
468 def GetBranch(self):
469 """Returns the short branch name, e.g. 'master'."""
470 if not self.branch:
471 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
472 self.branch = ShortBranchName(self.branchref)
473 return self.branch
474
475 def GetBranchRef(self):
476 """Returns the full branch name, e.g. 'refs/heads/master'."""
477 self.GetBranch() # Poke the lazy loader.
478 return self.branchref
479
[email protected]0f58fa82012-11-05 01:45:20480 @staticmethod
481 def FetchUpstreamTuple(branch):
[email protected]d6617f32013-11-19 00:34:54482 """Returns a tuple containing remote and remote ref,
[email protected]cc51cd02010-12-23 00:48:39483 e.g. 'origin', 'refs/heads/master'
484 """
485 remote = '.'
[email protected]cc51cd02010-12-23 00:48:39486 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
487 error_ok=True).strip()
488 if upstream_branch:
489 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
490 else:
[email protected]ade368c2011-03-01 08:57:50491 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
492 error_ok=True).strip()
493 if upstream_branch:
494 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
[email protected]cc51cd02010-12-23 00:48:39495 else:
[email protected]ade368c2011-03-01 08:57:50496 # Fall back on trying a git-svn upstream branch.
497 if settings.GetIsGitSvn():
498 upstream_branch = settings.GetSVNBranch()
[email protected]cc51cd02010-12-23 00:48:39499 else:
[email protected]ade368c2011-03-01 08:57:50500 # Else, try to guess the origin remote.
501 remote_branches = RunGit(['branch', '-r']).split()
502 if 'origin/master' in remote_branches:
503 # Fall back on origin/master if it exits.
504 remote = 'origin'
505 upstream_branch = 'refs/heads/master'
506 elif 'origin/trunk' in remote_branches:
507 # Fall back on origin/trunk if it exists. Generally a shared
508 # git-svn clone
509 remote = 'origin'
510 upstream_branch = 'refs/heads/trunk'
511 else:
512 DieWithError("""Unable to determine default branch to diff against.
[email protected]cc51cd02010-12-23 00:48:39513Either pass complete "git diff"-style arguments, like
514 git cl upload origin/master
515or verify this branch is set up to track another (via the --track argument to
516"git checkout -b ...").""")
517
518 return remote, upstream_branch
519
520 def GetUpstreamBranch(self):
521 if self.upstream_branch is None:
[email protected]0f58fa82012-11-05 01:45:20522 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
[email protected]cc51cd02010-12-23 00:48:39523 if remote is not '.':
524 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
525 self.upstream_branch = upstream_branch
526 return self.upstream_branch
527
[email protected]0f58fa82012-11-05 01:45:20528 def GetRemoteBranch(self):
[email protected]a2cbbbb2012-03-22 20:40:40529 if not self._remote:
[email protected]0f58fa82012-11-05 01:45:20530 remote, branch = None, self.GetBranch()
531 seen_branches = set()
532 while branch not in seen_branches:
533 seen_branches.add(branch)
534 remote, branch = self.FetchUpstreamTuple(branch)
535 branch = ShortBranchName(branch)
536 if remote != '.' or branch.startswith('refs/remotes'):
537 break
538 else:
[email protected]a2cbbbb2012-03-22 20:40:40539 remotes = RunGit(['remote'], error_ok=True).split()
540 if len(remotes) == 1:
[email protected]0f58fa82012-11-05 01:45:20541 remote, = remotes
[email protected]a2cbbbb2012-03-22 20:40:40542 elif 'origin' in remotes:
[email protected]0f58fa82012-11-05 01:45:20543 remote = 'origin'
[email protected]a2cbbbb2012-03-22 20:40:40544 logging.warning('Could not determine which remote this change is '
545 'associated with, so defaulting to "%s". This may '
546 'not be what you want. You may prevent this message '
547 'by running "git svn info" as documented here: %s',
548 self._remote,
549 GIT_INSTRUCTIONS_URL)
550 else:
551 logging.warn('Could not determine which remote this change is '
552 'associated with. You may prevent this message by '
553 'running "git svn info" as documented here: %s',
554 GIT_INSTRUCTIONS_URL)
[email protected]0f58fa82012-11-05 01:45:20555 branch = 'HEAD'
556 if branch.startswith('refs/remotes'):
557 self._remote = (remote, branch)
558 else:
559 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
[email protected]a2cbbbb2012-03-22 20:40:40560 return self._remote
561
[email protected]0f58fa82012-11-05 01:45:20562 def GitSanityChecks(self, upstream_git_obj):
563 """Checks git repo status and ensures diff is from local commits."""
564
565 # Verify the commit we're diffing against is in our current branch.
566 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
567 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
568 if upstream_sha != common_ancestor:
569 print >> sys.stderr, (
570 'ERROR: %s is not in the current branch. You may need to rebase '
571 'your tracking branch' % upstream_sha)
572 return False
573
574 # List the commits inside the diff, and verify they are all local.
575 commits_in_diff = RunGit(
576 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
577 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
578 remote_branch = remote_branch.strip()
579 if code != 0:
580 _, remote_branch = self.GetRemoteBranch()
581
582 commits_in_remote = RunGit(
583 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
584
585 common_commits = set(commits_in_diff) & set(commits_in_remote)
586 if common_commits:
587 print >> sys.stderr, (
588 'ERROR: Your diff contains %d commits already in %s.\n'
589 'Run "git log --oneline %s..HEAD" to get a list of commits in '
590 'the diff. If you are using a custom git flow, you can override'
591 ' the reference used for this check with "git config '
592 'gitcl.remotebranch <git-ref>".' % (
593 len(common_commits), remote_branch, upstream_git_obj))
594 return False
595 return True
596
[email protected]6b0051e2012-04-03 15:45:08597 def GetGitBaseUrlFromConfig(self):
598 """Return the configured base URL from branch.<branchname>.baseurl.
599
600 Returns None if it is not set.
601 """
602 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
603 error_ok=True).strip()
[email protected]a2cbbbb2012-03-22 20:40:40604
[email protected]cc51cd02010-12-23 00:48:39605 def GetRemoteUrl(self):
606 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
607
608 Returns None if there is no remote.
609 """
[email protected]0f58fa82012-11-05 01:45:20610 remote, _ = self.GetRemoteBranch()
[email protected]cc51cd02010-12-23 00:48:39611 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
612
613 def GetIssue(self):
[email protected]52424302012-08-29 15:14:30614 """Returns the issue number as a int or None if not set."""
[email protected]1033efd2013-07-23 23:25:09615 if self.issue is None and not self.lookedup_issue:
[email protected]cc51cd02010-12-23 00:48:39616 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
[email protected]1033efd2013-07-23 23:25:09617 self.issue = int(issue) or None if issue else None
618 self.lookedup_issue = True
[email protected]cc51cd02010-12-23 00:48:39619 return self.issue
620
621 def GetRietveldServer(self):
[email protected]0af9b702012-02-11 00:42:16622 if not self.rietveld_server:
623 # If we're on a branch then get the server potentially associated
624 # with that branch.
625 if self.GetIssue():
626 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
627 ['config', self._RietveldServer()], error_ok=True).strip())
628 if not self.rietveld_server:
629 self.rietveld_server = settings.GetDefaultServerUrl()
[email protected]cc51cd02010-12-23 00:48:39630 return self.rietveld_server
631
632 def GetIssueURL(self):
633 """Get the URL for a particular issue."""
[email protected]015fd3d2013-06-18 19:02:50634 if not self.GetIssue():
635 return None
[email protected]cc51cd02010-12-23 00:48:39636 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
637
638 def GetDescription(self, pretty=False):
639 if not self.has_description:
640 if self.GetIssue():
[email protected]52424302012-08-29 15:14:30641 issue = self.GetIssue()
[email protected]183df1a2012-01-04 19:44:55642 try:
643 self.description = self.RpcServer().get_description(issue).strip()
644 except urllib2.HTTPError, e:
645 if e.code == 404:
646 DieWithError(
647 ('\nWhile fetching the description for issue %d, received a '
648 '404 (not found)\n'
649 'error. It is likely that you deleted this '
650 'issue on the server. If this is the\n'
651 'case, please run\n\n'
652 ' git cl issue 0\n\n'
653 'to clear the association with the deleted issue. Then run '
654 'this command again.') % issue)
655 else:
656 DieWithError(
[email protected]daee1d32013-12-18 11:55:03657 '\nFailed to fetch issue description. HTTP error %d' % e.code)
[email protected]cc51cd02010-12-23 00:48:39658 self.has_description = True
659 if pretty:
660 wrapper = textwrap.TextWrapper()
661 wrapper.initial_indent = wrapper.subsequent_indent = ' '
662 return wrapper.fill(self.description)
663 return self.description
664
665 def GetPatchset(self):
[email protected]52424302012-08-29 15:14:30666 """Returns the patchset number as a int or None if not set."""
[email protected]1033efd2013-07-23 23:25:09667 if self.patchset is None and not self.lookedup_patchset:
[email protected]cc51cd02010-12-23 00:48:39668 patchset = RunGit(['config', self._PatchsetSetting()],
669 error_ok=True).strip()
[email protected]1033efd2013-07-23 23:25:09670 self.patchset = int(patchset) or None if patchset else None
671 self.lookedup_patchset = True
[email protected]cc51cd02010-12-23 00:48:39672 return self.patchset
673
674 def SetPatchset(self, patchset):
675 """Set this branch's patchset. If patchset=0, clears the patchset."""
676 if patchset:
677 RunGit(['config', self._PatchsetSetting(), str(patchset)])
[email protected]1033efd2013-07-23 23:25:09678 self.patchset = patchset
[email protected]cc51cd02010-12-23 00:48:39679 else:
680 RunGit(['config', '--unset', self._PatchsetSetting()],
[email protected]32f9f5e2011-09-14 13:41:47681 stderr=subprocess2.PIPE, error_ok=True)
[email protected]1033efd2013-07-23 23:25:09682 self.patchset = None
[email protected]cc51cd02010-12-23 00:48:39683
[email protected]1033efd2013-07-23 23:25:09684 def GetMostRecentPatchset(self):
685 return self.GetIssueProperties()['patchsets'][-1]
[email protected]0281f522012-09-14 13:37:59686
687 def GetPatchSetDiff(self, issue, patchset):
[email protected]27bb3872011-05-30 20:33:19688 return self.RpcServer().get(
[email protected]e77ebbf2011-03-29 20:35:38689 '/download/issue%s_%s.diff' % (issue, patchset))
690
[email protected]1033efd2013-07-23 23:25:09691 def GetIssueProperties(self):
692 if self._props is None:
693 issue = self.GetIssue()
694 if not issue:
695 self._props = {}
696 else:
697 self._props = self.RpcServer().get_issue_properties(issue, True)
698 return self._props
699
[email protected]cf087782013-07-23 13:08:48700 def GetApprovingReviewers(self):
[email protected]1033efd2013-07-23 23:25:09701 return get_approving_reviewers(self.GetIssueProperties())
[email protected]e52678e2013-04-26 18:34:44702
[email protected]cc51cd02010-12-23 00:48:39703 def SetIssue(self, issue):
704 """Set this branch's issue. If issue=0, clears the issue."""
705 if issue:
[email protected]1033efd2013-07-23 23:25:09706 self.issue = issue
[email protected]cc51cd02010-12-23 00:48:39707 RunGit(['config', self._IssueSetting(), str(issue)])
708 if self.rietveld_server:
709 RunGit(['config', self._RietveldServer(), self.rietveld_server])
710 else:
[email protected]d79d4b82013-10-23 20:09:08711 current_issue = self.GetIssue()
712 if current_issue:
713 RunGit(['config', '--unset', self._IssueSetting()])
[email protected]1033efd2013-07-23 23:25:09714 self.issue = None
715 self.SetPatchset(None)
[email protected]cc51cd02010-12-23 00:48:39716
[email protected]15169952011-09-27 14:30:53717 def GetChange(self, upstream_branch, author):
[email protected]0f58fa82012-11-05 01:45:20718 if not self.GitSanityChecks(upstream_branch):
719 DieWithError('\nGit sanity check failure')
720
[email protected]82b91cd2013-07-09 06:33:41721 env = os.environ.copy()
722 # 'cat' is a magical git string that disables pagers on all platforms.
723 env['GIT_PAGER'] = 'cat'
724
725 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
[email protected]f267b0e2013-05-02 09:11:43726 if not root:
727 root = '.'
[email protected]512f1ef2011-04-20 15:17:57728 absroot = os.path.abspath(root)
[email protected]6fb99c62011-04-18 15:57:28729
730 # We use the sha1 of HEAD as a name of this change.
[email protected]82b91cd2013-07-09 06:33:41731 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
[email protected]512f1ef2011-04-20 15:17:57732 # Need to pass a relative path for msysgit.
[email protected]2b38e9c2011-10-19 00:04:35733 try:
[email protected]80a9ef12011-12-13 20:44:10734 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
[email protected]2b38e9c2011-10-19 00:04:35735 except subprocess2.CalledProcessError:
736 DieWithError(
[email protected]d6617f32013-11-19 00:34:54737 ('\nFailed to diff against upstream branch %s\n\n'
[email protected]2b38e9c2011-10-19 00:04:35738 'This branch probably doesn\'t exist anymore. To reset the\n'
739 'tracking branch, please run\n'
740 ' git branch --set-upstream %s trunk\n'
741 'replacing trunk with origin/master or the relevant branch') %
742 (upstream_branch, self.GetBranch()))
[email protected]6fb99c62011-04-18 15:57:28743
[email protected]52424302012-08-29 15:14:30744 issue = self.GetIssue()
745 patchset = self.GetPatchset()
[email protected]6fb99c62011-04-18 15:57:28746 if issue:
747 description = self.GetDescription()
748 else:
749 # If the change was never uploaded, use the log messages of all commits
750 # up to the branch point, as git cl upload will prefill the description
751 # with these log messages.
[email protected]82b91cd2013-07-09 06:33:41752 description = RunCommand(['git',
[email protected]f267b0e2013-05-02 09:11:43753 'log', '--pretty=format:%s%n%n%b',
[email protected]82b91cd2013-07-09 06:33:41754 '%s...' % (upstream_branch)],
755 env=env).strip()
[email protected]03b3bdc2011-06-14 13:04:12756
757 if not author:
[email protected]13f623c2011-07-22 16:02:23758 author = RunGit(['config', 'user.email']).strip() or None
[email protected]15169952011-09-27 14:30:53759 return presubmit_support.GitChange(
[email protected]6fb99c62011-04-18 15:57:28760 name,
761 description,
762 absroot,
763 files,
764 issue,
765 patchset,
[email protected]03b3bdc2011-06-14 13:04:12766 author)
[email protected]6fb99c62011-04-18 15:57:28767
[email protected]051ad0e2013-03-04 21:57:34768 def RunHook(self, committing, may_prompt, verbose, change):
[email protected]15169952011-09-27 14:30:53769 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
[email protected]6fb99c62011-04-18 15:57:28770
771 try:
[email protected]b0a63912012-01-17 18:10:16772 return presubmit_support.DoPresubmitChecks(change, committing,
[email protected]6fb99c62011-04-18 15:57:28773 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
[email protected]cc73ad62011-07-06 17:39:26774 default_presubmit=None, may_prompt=may_prompt,
[email protected]239f4112011-06-03 20:08:23775 rietveld_obj=self.RpcServer())
[email protected]6fb99c62011-04-18 15:57:28776 except presubmit_support.PresubmitFailure, e:
777 DieWithError(
778 ('%s\nMaybe your depot_tools is out of date?\n'
779 'If all fails, contact maruel@') % e)
780
[email protected]b021b322013-04-08 17:57:29781 def UpdateDescription(self, description):
782 self.description = description
783 return self.RpcServer().update_description(
784 self.GetIssue(), self.description)
785
[email protected]cc51cd02010-12-23 00:48:39786 def CloseIssue(self):
[email protected]607bb1b2011-06-01 23:43:11787 """Updates the description and closes the issue."""
[email protected]b021b322013-04-08 17:57:29788 return self.RpcServer().close_issue(self.GetIssue())
[email protected]cc51cd02010-12-23 00:48:39789
[email protected]27bb3872011-05-30 20:33:19790 def SetFlag(self, flag, value):
791 """Patchset must match."""
792 if not self.GetPatchset():
793 DieWithError('The patchset needs to match. Send another patchset.')
794 try:
795 return self.RpcServer().set_flag(
[email protected]52424302012-08-29 15:14:30796 self.GetIssue(), self.GetPatchset(), flag, value)
[email protected]27bb3872011-05-30 20:33:19797 except urllib2.HTTPError, e:
798 if e.code == 404:
799 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
800 if e.code == 403:
801 DieWithError(
802 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
803 'match?') % (self.GetIssue(), self.GetPatchset()))
804 raise
[email protected]cc51cd02010-12-23 00:48:39805
[email protected]cab38e92011-04-09 00:30:51806 def RpcServer(self):
[email protected]cc51cd02010-12-23 00:48:39807 """Returns an upload.RpcServer() to access this review's rietveld instance.
808 """
[email protected]e77ebbf2011-03-29 20:35:38809 if not self._rpc_server:
[email protected]4bac4b52012-11-27 20:33:52810 self._rpc_server = rietveld.CachingRietveld(
811 self.GetRietveldServer(), None, None)
[email protected]e77ebbf2011-03-29 20:35:38812 return self._rpc_server
[email protected]cc51cd02010-12-23 00:48:39813
814 def _IssueSetting(self):
815 """Return the git setting that stores this change's issue."""
816 return 'branch.%s.rietveldissue' % self.GetBranch()
817
818 def _PatchsetSetting(self):
819 """Return the git setting that stores this change's most recent patchset."""
820 return 'branch.%s.rietveldpatchset' % self.GetBranch()
821
822 def _RietveldServer(self):
823 """Returns the git setting that stores this change's rietveld server."""
824 return 'branch.%s.rietveldserver' % self.GetBranch()
825
826
827def GetCodereviewSettingsInteractively():
828 """Prompt the user for settings."""
[email protected]e8077812012-02-03 03:41:46829 # TODO(ukai): ask code review system is rietveld or gerrit?
[email protected]cc51cd02010-12-23 00:48:39830 server = settings.GetDefaultServerUrl(error_ok=True)
831 prompt = 'Rietveld server (host[:port])'
832 prompt += ' [%s]' % (server or DEFAULT_SERVER)
[email protected]90541732011-04-01 17:54:18833 newserver = ask_for_data(prompt + ':')
[email protected]cc51cd02010-12-23 00:48:39834 if not server and not newserver:
835 newserver = DEFAULT_SERVER
[email protected]eb5edbc2012-01-16 17:03:28836 if newserver:
837 newserver = gclient_utils.UpgradeToHttps(newserver)
838 if newserver != server:
839 RunGit(['config', 'rietveld.server', newserver])
[email protected]cc51cd02010-12-23 00:48:39840
[email protected]eb5edbc2012-01-16 17:03:28841 def SetProperty(initial, caption, name, is_url):
[email protected]cc51cd02010-12-23 00:48:39842 prompt = caption
843 if initial:
844 prompt += ' ("x" to clear) [%s]' % initial
[email protected]90541732011-04-01 17:54:18845 new_val = ask_for_data(prompt + ':')
[email protected]cc51cd02010-12-23 00:48:39846 if new_val == 'x':
847 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
[email protected]eb5edbc2012-01-16 17:03:28848 elif new_val:
849 if is_url:
850 new_val = gclient_utils.UpgradeToHttps(new_val)
851 if new_val != initial:
852 RunGit(['config', 'rietveld.' + name, new_val])
[email protected]cc51cd02010-12-23 00:48:39853
[email protected]eb5edbc2012-01-16 17:03:28854 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
[email protected]c1737d02013-05-29 14:17:28855 SetProperty(settings.GetDefaultPrivateFlag(),
856 'Private flag (rietveld only)', 'private', False)
[email protected]cc51cd02010-12-23 00:48:39857 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
[email protected]eb5edbc2012-01-16 17:03:28858 'tree-status-url', False)
859 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
[email protected]cc51cd02010-12-23 00:48:39860
861 # TODO: configure a default branch to diff against, rather than this
862 # svn-based hackery.
863
864
[email protected]20254fc2011-03-22 18:28:59865class ChangeDescription(object):
866 """Contains a parsed form of the change description."""
[email protected]c6f60e82013-04-19 17:01:57867 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
[email protected]42c20792013-09-12 17:34:49868 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
[email protected]20254fc2011-03-22 18:28:59869
[email protected]78936cb2013-04-11 00:17:52870 def __init__(self, description):
[email protected]42c20792013-09-12 17:34:49871 self._description_lines = (description or '').strip().splitlines()
[email protected]78936cb2013-04-11 00:17:52872
[email protected]42c20792013-09-12 17:34:49873 @property # www.logilab.org/ticket/89786
874 def description(self): # pylint: disable=E0202
875 return '\n'.join(self._description_lines)
876
877 def set_description(self, desc):
878 if isinstance(desc, basestring):
879 lines = desc.splitlines()
880 else:
881 lines = [line.rstrip() for line in desc]
882 while lines and not lines[0]:
883 lines.pop(0)
884 while lines and not lines[-1]:
885 lines.pop(-1)
886 self._description_lines = lines
[email protected]78936cb2013-04-11 00:17:52887
888 def update_reviewers(self, reviewers):
[email protected]42c20792013-09-12 17:34:49889 """Rewrites the R=/TBR= line(s) as a single line each."""
[email protected]78936cb2013-04-11 00:17:52890 assert isinstance(reviewers, list), reviewers
891 if not reviewers:
892 return
[email protected]42c20792013-09-12 17:34:49893 reviewers = reviewers[:]
[email protected]78936cb2013-04-11 00:17:52894
[email protected]42c20792013-09-12 17:34:49895 # Get the set of R= and TBR= lines and remove them from the desciption.
896 regexp = re.compile(self.R_LINE)
897 matches = [regexp.match(line) for line in self._description_lines]
898 new_desc = [l for i, l in enumerate(self._description_lines)
899 if not matches[i]]
900 self.set_description(new_desc)
[email protected]78936cb2013-04-11 00:17:52901
[email protected]42c20792013-09-12 17:34:49902 # Construct new unified R= and TBR= lines.
903 r_names = []
904 tbr_names = []
905 for match in matches:
906 if not match:
907 continue
908 people = cleanup_list([match.group(2).strip()])
909 if match.group(1) == 'TBR':
910 tbr_names.extend(people)
911 else:
912 r_names.extend(people)
913 for name in r_names:
914 if name not in reviewers:
915 reviewers.append(name)
916 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
917 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
918
919 # Put the new lines in the description where the old first R= line was.
920 line_loc = next((i for i, match in enumerate(matches) if match), -1)
921 if 0 <= line_loc < len(self._description_lines):
922 if new_tbr_line:
923 self._description_lines.insert(line_loc, new_tbr_line)
924 if new_r_line:
925 self._description_lines.insert(line_loc, new_r_line)
[email protected]78936cb2013-04-11 00:17:52926 else:
[email protected]42c20792013-09-12 17:34:49927 if new_r_line:
928 self.append_footer(new_r_line)
929 if new_tbr_line:
930 self.append_footer(new_tbr_line)
[email protected]78936cb2013-04-11 00:17:52931
932 def prompt(self):
933 """Asks the user to update the description."""
[email protected]42c20792013-09-12 17:34:49934 self.set_description([
935 '# Enter a description of the change.',
936 '# This will be displayed on the codereview site.',
937 '# The first line will also be used as the subject of the review.',
[email protected]bd1073e2013-06-01 00:34:38938 '#--------------------This line is 72 characters long'
[email protected]42c20792013-09-12 17:34:49939 '--------------------',
940 ] + self._description_lines)
[email protected]78936cb2013-04-11 00:17:52941
[email protected]42c20792013-09-12 17:34:49942 regexp = re.compile(self.BUG_LINE)
943 if not any((regexp.match(line) for line in self._description_lines)):
[email protected]78936cb2013-04-11 00:17:52944 self.append_footer('BUG=')
[email protected]42c20792013-09-12 17:34:49945 content = gclient_utils.RunEditor(self.description, True,
[email protected]615a2622013-05-03 13:20:14946 git_editor=settings.GetGitEditor())
[email protected]0e0436a2011-10-25 13:32:41947 if not content:
948 DieWithError('Running editor failed')
[email protected]42c20792013-09-12 17:34:49949 lines = content.splitlines()
[email protected]78936cb2013-04-11 00:17:52950
951 # Strip off comments.
[email protected]42c20792013-09-12 17:34:49952 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
953 if not clean_lines:
[email protected]0e0436a2011-10-25 13:32:41954 DieWithError('No CL description, aborting')
[email protected]42c20792013-09-12 17:34:49955 self.set_description(clean_lines)
[email protected]20254fc2011-03-22 18:28:59956
[email protected]78936cb2013-04-11 00:17:52957 def append_footer(self, line):
[email protected]42c20792013-09-12 17:34:49958 if self._description_lines:
959 # Add an empty line if either the last line or the new line isn't a tag.
960 last_line = self._description_lines[-1]
961 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
962 not presubmit_support.Change.TAG_LINE_RE.match(line)):
963 self._description_lines.append('')
964 self._description_lines.append(line)
[email protected]20254fc2011-03-22 18:28:59965
[email protected]78936cb2013-04-11 00:17:52966 def get_reviewers(self):
967 """Retrieves the list of reviewers."""
[email protected]42c20792013-09-12 17:34:49968 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
969 reviewers = [match.group(2).strip() for match in matches if match]
[email protected]78936cb2013-04-11 00:17:52970 return cleanup_list(reviewers)
[email protected]20254fc2011-03-22 18:28:59971
972
[email protected]e52678e2013-04-26 18:34:44973def get_approving_reviewers(props):
974 """Retrieves the reviewers that approved a CL from the issue properties with
975 messages.
976
977 Note that the list may contain reviewers that are not committer, thus are not
978 considered by the CQ.
979 """
980 return sorted(
981 set(
982 message['sender']
983 for message in props['messages']
984 if message['approval'] and message['sender'] in props['reviewers']
985 )
986 )
987
988
[email protected]cc51cd02010-12-23 00:48:39989def FindCodereviewSettingsFile(filename='codereview.settings'):
990 """Finds the given file starting in the cwd and going up.
991
992 Only looks up to the top of the repository unless an
993 'inherit-review-settings-ok' file exists in the root of the repository.
994 """
995 inherit_ok_file = 'inherit-review-settings-ok'
996 cwd = os.getcwd()
997 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
998 if os.path.isfile(os.path.join(root, inherit_ok_file)):
999 root = '/'
1000 while True:
1001 if filename in os.listdir(cwd):
1002 if os.path.isfile(os.path.join(cwd, filename)):
1003 return open(os.path.join(cwd, filename))
1004 if cwd == root:
1005 break
1006 cwd = os.path.dirname(cwd)
1007
1008
1009def LoadCodereviewSettingsFromFile(fileobj):
1010 """Parse a codereview.settings file and updates hooks."""
[email protected]99ac1c52012-01-16 14:52:121011 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
[email protected]cc51cd02010-12-23 00:48:391012
[email protected]cc51cd02010-12-23 00:48:391013 def SetProperty(name, setting, unset_error_ok=False):
1014 fullname = 'rietveld.' + name
1015 if setting in keyvals:
1016 RunGit(['config', fullname, keyvals[setting]])
1017 else:
1018 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1019
1020 SetProperty('server', 'CODE_REVIEW_SERVER')
1021 # Only server setting is required. Other settings can be absent.
1022 # In that case, we ignore errors raised during option deletion attempt.
1023 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
[email protected]c1737d02013-05-29 14:17:281024 SetProperty('private', 'PRIVATE', unset_error_ok=True)
[email protected]cc51cd02010-12-23 00:48:391025 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1026 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1027
[email protected]7044efc2013-11-28 01:51:211028 if 'GERRIT_HOST' in keyvals:
[email protected]e8077812012-02-03 03:41:461029 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
[email protected]e8077812012-02-03 03:41:461030
[email protected]cc51cd02010-12-23 00:48:391031 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1032 #should be of the form
1033 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1034 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1035 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1036 keyvals['ORIGIN_URL_CONFIG']])
1037
[email protected]cc51cd02010-12-23 00:48:391038
[email protected]426f69b2012-08-02 23:41:491039def urlretrieve(source, destination):
1040 """urllib is broken for SSL connections via a proxy therefore we
1041 can't use urllib.urlretrieve()."""
1042 with open(destination, 'w') as f:
1043 f.write(urllib2.urlopen(source).read())
1044
1045
[email protected]712d6102013-11-27 00:52:581046def hasSheBang(fname):
1047 """Checks fname is a #! script."""
1048 with open(fname) as f:
1049 return f.read(2).startswith('#!')
1050
1051
[email protected]78c4b982012-02-14 02:20:261052def DownloadHooks(force):
1053 """downloads hooks
1054
1055 Args:
1056 force: True to update hooks. False to install hooks if not present.
1057 """
1058 if not settings.GetIsGerrit():
1059 return
[email protected]712d6102013-11-27 00:52:581060 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
[email protected]78c4b982012-02-14 02:20:261061 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1062 if not os.access(dst, os.X_OK):
1063 if os.path.exists(dst):
1064 if not force:
1065 return
[email protected]78c4b982012-02-14 02:20:261066 try:
[email protected]426f69b2012-08-02 23:41:491067 urlretrieve(src, dst)
[email protected]712d6102013-11-27 00:52:581068 if not hasSheBang(dst):
1069 DieWithError('Not a script: %s\n'
1070 'You need to download from\n%s\n'
1071 'into .git/hooks/commit-msg and '
1072 'chmod +x .git/hooks/commit-msg' % (dst, src))
[email protected]78c4b982012-02-14 02:20:261073 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1074 except Exception:
1075 if os.path.exists(dst):
1076 os.remove(dst)
[email protected]712d6102013-11-27 00:52:581077 DieWithError('\nFailed to download hooks.\n'
1078 'You need to download from\n%s\n'
1079 'into .git/hooks/commit-msg and '
1080 'chmod +x .git/hooks/commit-msg' % src)
[email protected]78c4b982012-02-14 02:20:261081
1082
[email protected]0633fb42013-08-16 20:06:141083@subcommand.usage('[repo root containing codereview.settings]')
[email protected]cc51cd02010-12-23 00:48:391084def CMDconfig(parser, args):
[email protected]d9c1b202013-07-24 23:52:111085 """Edits configuration for this tree."""
[email protected]cc51cd02010-12-23 00:48:391086
[email protected]97ae58e2011-03-18 00:29:201087 _, args = parser.parse_args(args)
[email protected]cc51cd02010-12-23 00:48:391088 if len(args) == 0:
1089 GetCodereviewSettingsInteractively()
[email protected]78c4b982012-02-14 02:20:261090 DownloadHooks(True)
[email protected]cc51cd02010-12-23 00:48:391091 return 0
1092
1093 url = args[0]
1094 if not url.endswith('codereview.settings'):
1095 url = os.path.join(url, 'codereview.settings')
1096
1097 # Load code review settings and download hooks (if available).
1098 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
[email protected]78c4b982012-02-14 02:20:261099 DownloadHooks(True)
[email protected]cc51cd02010-12-23 00:48:391100 return 0
1101
1102
[email protected]6b0051e2012-04-03 15:45:081103def CMDbaseurl(parser, args):
[email protected]d9c1b202013-07-24 23:52:111104 """Gets or sets base-url for this branch."""
[email protected]6b0051e2012-04-03 15:45:081105 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1106 branch = ShortBranchName(branchref)
1107 _, args = parser.parse_args(args)
1108 if not args:
1109 print("Current base-url:")
1110 return RunGit(['config', 'branch.%s.base-url' % branch],
1111 error_ok=False).strip()
1112 else:
1113 print("Setting base-url to %s" % args[0])
1114 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1115 error_ok=False).strip()
1116
1117
[email protected]cc51cd02010-12-23 00:48:391118def CMDstatus(parser, args):
[email protected]d9c1b202013-07-24 23:52:111119 """Show status of changelists.
1120
1121 Colors are used to tell the state of the CL unless --fast is used:
[email protected]aeab41a2013-12-10 20:01:221122 - Red not sent for review or broken
1123 - Blue waiting for review
1124 - Yellow waiting for you to reply to review
1125 - Green LGTM'ed
1126 - Magenta in the commit queue
1127 - Cyan was committed, branch can be deleted
[email protected]d9c1b202013-07-24 23:52:111128
1129 Also see 'git cl comments'.
1130 """
[email protected]cc51cd02010-12-23 00:48:391131 parser.add_option('--field',
1132 help='print only specific field (desc|id|patch|url)')
[email protected]1033efd2013-07-23 23:25:091133 parser.add_option('-f', '--fast', action='store_true',
1134 help='Do not retrieve review status')
[email protected]cc51cd02010-12-23 00:48:391135 (options, args) = parser.parse_args(args)
[email protected]39c0b222013-08-17 16:57:011136 if args:
1137 parser.error('Unsupported args: %s' % args)
[email protected]cc51cd02010-12-23 00:48:391138
[email protected]cc51cd02010-12-23 00:48:391139 if options.field:
[email protected]e25c75b2013-07-23 18:30:561140 cl = Changelist()
[email protected]cc51cd02010-12-23 00:48:391141 if options.field.startswith('desc'):
1142 print cl.GetDescription()
1143 elif options.field == 'id':
1144 issueid = cl.GetIssue()
1145 if issueid:
1146 print issueid
1147 elif options.field == 'patch':
1148 patchset = cl.GetPatchset()
1149 if patchset:
1150 print patchset
1151 elif options.field == 'url':
1152 url = cl.GetIssueURL()
1153 if url:
1154 print url
[email protected]e25c75b2013-07-23 18:30:561155 return 0
1156
1157 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1158 if not branches:
1159 print('No local branch found.')
1160 return 0
1161
1162 changes = (Changelist(branchref=b) for b in branches.splitlines())
1163 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1164 alignment = max(5, max(len(b) for b in branches))
1165 print 'Branches associated with reviews:'
[email protected]1033efd2013-07-23 23:25:091166 # Adhoc thread pool to request data concurrently.
1167 output = Queue.Queue()
1168
1169 # Silence upload.py otherwise it becomes unweldly.
1170 upload.verbosity = 0
1171
1172 if not options.fast:
1173 def fetch(b):
[email protected]d9c1b202013-07-24 23:52:111174 """Fetches information for an issue and returns (branch, issue, color)."""
[email protected]1033efd2013-07-23 23:25:091175 c = Changelist(branchref=b)
1176 i = c.GetIssueURL()
[email protected]d9c1b202013-07-24 23:52:111177 props = {}
1178 r = None
1179 if i:
1180 try:
1181 props = c.GetIssueProperties()
1182 r = c.GetApprovingReviewers() if i else None
1183 except urllib2.HTTPError:
1184 # The issue probably doesn't exist anymore.
1185 i += ' (broken)'
1186
1187 msgs = props.get('messages') or []
1188
1189 if not i:
1190 color = Fore.WHITE
1191 elif props.get('closed'):
1192 # Issue is closed.
1193 color = Fore.CYAN
[email protected]aeab41a2013-12-10 20:01:221194 elif props.get('commit'):
1195 # Issue is in the commit queue.
1196 color = Fore.MAGENTA
[email protected]d9c1b202013-07-24 23:52:111197 elif r:
1198 # Was LGTM'ed.
1199 color = Fore.GREEN
1200 elif not msgs:
1201 # No message was sent.
1202 color = Fore.RED
1203 elif msgs[-1]['sender'] != props.get('owner_email'):
1204 color = Fore.YELLOW
1205 else:
1206 color = Fore.BLUE
1207 output.put((b, i, color))
[email protected]1033efd2013-07-23 23:25:091208
1209 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1210 for t in threads:
1211 t.daemon = True
1212 t.start()
1213 else:
1214 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1215 for b in branches:
1216 c = Changelist(branchref=b)
[email protected]d9c1b202013-07-24 23:52:111217 url = c.GetIssueURL()
1218 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
[email protected]1033efd2013-07-23 23:25:091219
1220 tmp = {}
1221 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
[email protected]e25c75b2013-07-23 18:30:561222 for branch in sorted(branches):
[email protected]1033efd2013-07-23 23:25:091223 while branch not in tmp:
[email protected]d9c1b202013-07-24 23:52:111224 b, i, color = output.get()
1225 tmp[b] = (i, color)
1226 issue, color = tmp.pop(branch)
[email protected]885f6512013-07-27 02:17:261227 reset = Fore.RESET
1228 if not sys.stdout.isatty():
1229 color = ''
1230 reset = ''
[email protected]c3d17dd2013-12-19 00:55:311231 print ' %*s : %s%s%s' % (
[email protected]885f6512013-07-27 02:17:261232 alignment, ShortBranchName(branch), color, issue, reset)
[email protected]1033efd2013-07-23 23:25:091233
[email protected]e25c75b2013-07-23 18:30:561234 cl = Changelist()
1235 print
1236 print 'Current branch:',
1237 if not cl.GetIssue():
1238 print 'no issue assigned.'
1239 return 0
1240 print cl.GetBranch()
1241 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1242 print 'Issue description:'
1243 print cl.GetDescription(pretty=True)
[email protected]cc51cd02010-12-23 00:48:391244 return 0
1245
1246
[email protected]39c0b222013-08-17 16:57:011247def colorize_CMDstatus_doc():
1248 """To be called once in main() to add colors to git cl status help."""
1249 colors = [i for i in dir(Fore) if i[0].isupper()]
1250
1251 def colorize_line(line):
1252 for color in colors:
1253 if color in line.upper():
1254 # Extract whitespaces first and the leading '-'.
1255 indent = len(line) - len(line.lstrip(' ')) + 1
1256 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1257 return line
1258
1259 lines = CMDstatus.__doc__.splitlines()
1260 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1261
1262
[email protected]0633fb42013-08-16 20:06:141263@subcommand.usage('[issue_number]')
[email protected]cc51cd02010-12-23 00:48:391264def CMDissue(parser, args):
[email protected]d9c1b202013-07-24 23:52:111265 """Sets or displays the current code review issue number.
[email protected]cc51cd02010-12-23 00:48:391266
1267 Pass issue number 0 to clear the current issue.
[email protected]d9c1b202013-07-24 23:52:111268 """
[email protected]97ae58e2011-03-18 00:29:201269 _, args = parser.parse_args(args)
[email protected]cc51cd02010-12-23 00:48:391270
1271 cl = Changelist()
1272 if len(args) > 0:
1273 try:
1274 issue = int(args[0])
1275 except ValueError:
1276 DieWithError('Pass a number to set the issue or none to list it.\n'
1277 'Maybe you want to run git cl status?')
1278 cl.SetIssue(issue)
[email protected]52424302012-08-29 15:14:301279 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
[email protected]cc51cd02010-12-23 00:48:391280 return 0
1281
1282
[email protected]9977a2e2012-06-06 22:30:561283def CMDcomments(parser, args):
[email protected]d9c1b202013-07-24 23:52:111284 """Shows review comments of the current changelist."""
[email protected]9977a2e2012-06-06 22:30:561285 (_, args) = parser.parse_args(args)
1286 if args:
1287 parser.error('Unsupported argument: %s' % args)
1288
1289 cl = Changelist()
1290 if cl.GetIssue():
[email protected]1033efd2013-07-23 23:25:091291 data = cl.GetIssueProperties()
[email protected]9977a2e2012-06-06 22:30:561292 for message in sorted(data['messages'], key=lambda x: x['date']):
[email protected]2e23ce32013-05-07 12:42:281293 if message['disapproval']:
1294 color = Fore.RED
1295 elif message['approval']:
1296 color = Fore.GREEN
1297 elif message['sender'] == data['owner_email']:
1298 color = Fore.MAGENTA
1299 else:
1300 color = Fore.BLUE
1301 print '\n%s%s %s%s' % (
1302 color, message['date'].split('.', 1)[0], message['sender'],
1303 Fore.RESET)
[email protected]9977a2e2012-06-06 22:30:561304 if message['text'].strip():
1305 print '\n'.join(' ' + l for l in message['text'].splitlines())
1306 return 0
1307
1308
[email protected]eec76592013-05-20 16:27:571309def CMDdescription(parser, args):
[email protected]d9c1b202013-07-24 23:52:111310 """Brings up the editor for the current CL's description."""
[email protected]eec76592013-05-20 16:27:571311 cl = Changelist()
1312 if not cl.GetIssue():
1313 DieWithError('This branch has no associated changelist.')
1314 description = ChangeDescription(cl.GetDescription())
1315 description.prompt()
1316 cl.UpdateDescription(description.description)
1317 return 0
1318
1319
[email protected]cc51cd02010-12-23 00:48:391320def CreateDescriptionFromLog(args):
1321 """Pulls out the commit log to use as a base for the CL description."""
1322 log_args = []
1323 if len(args) == 1 and not args[0].endswith('.'):
1324 log_args = [args[0] + '..']
1325 elif len(args) == 1 and args[0].endswith('...'):
1326 log_args = [args[0][:-1]]
1327 elif len(args) == 2:
1328 log_args = [args[0] + '..' + args[1]]
1329 else:
1330 log_args = args[:] # Hope for the best!
[email protected]373af802012-05-25 21:07:331331 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
[email protected]cc51cd02010-12-23 00:48:391332
1333
[email protected]cc51cd02010-12-23 00:48:391334def CMDpresubmit(parser, args):
[email protected]d9c1b202013-07-24 23:52:111335 """Runs presubmit tests on the current changelist."""
[email protected]375a9022013-01-07 01:12:051336 parser.add_option('-u', '--upload', action='store_true',
[email protected]cc51cd02010-12-23 00:48:391337 help='Run upload hook instead of the push/dcommit hook')
[email protected]375a9022013-01-07 01:12:051338 parser.add_option('-f', '--force', action='store_true',
[email protected]495ad152012-09-04 23:07:421339 help='Run checks even if tree is dirty')
[email protected]cc51cd02010-12-23 00:48:391340 (options, args) = parser.parse_args(args)
1341
[email protected]259e4682012-10-25 07:36:331342 if not options.force and is_dirty_git_tree('presubmit'):
1343 print 'use --force to check even if tree is dirty.'
[email protected]cc51cd02010-12-23 00:48:391344 return 1
1345
[email protected]970c5222011-03-12 00:32:241346 cl = Changelist()
[email protected]cc51cd02010-12-23 00:48:391347 if args:
1348 base_branch = args[0]
1349 else:
[email protected]0f58fa82012-11-05 01:45:201350 # Default to diffing against the common ancestor of the upstream branch.
1351 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
[email protected]cc51cd02010-12-23 00:48:391352
[email protected]051ad0e2013-03-04 21:57:341353 cl.RunHook(
1354 committing=not options.upload,
1355 may_prompt=False,
1356 verbose=options.verbose,
1357 change=cl.GetChange(base_branch, None))
[email protected]0a2bb372011-03-25 01:16:221358 return 0
[email protected]cc51cd02010-12-23 00:48:391359
1360
[email protected]aebe87f2012-10-22 20:34:211361def AddChangeIdToCommitMessage(options, args):
1362 """Re-commits using the current message, assumes the commit hook is in
1363 place.
1364 """
1365 log_desc = options.message or CreateDescriptionFromLog(args)
1366 git_command = ['commit', '--amend', '-m', log_desc]
1367 RunGit(git_command)
1368 new_log_desc = CreateDescriptionFromLog(args)
1369 if CHANGE_ID in new_log_desc:
1370 print 'git-cl: Added Change-Id to commit message.'
1371 else:
1372 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1373
1374
[email protected]e8077812012-02-03 03:41:461375def GerritUpload(options, args, cl):
1376 """upload the current branch to gerrit."""
1377 # We assume the remote called "origin" is the one we want.
1378 # It is probably not worthwhile to support different workflows.
1379 remote = 'origin'
1380 branch = 'master'
1381 if options.target_branch:
1382 branch = options.target_branch
[email protected]cc51cd02010-12-23 00:48:391383
[email protected]78936cb2013-04-11 00:17:521384 change_desc = ChangeDescription(
1385 options.message or CreateDescriptionFromLog(args))
1386 if not change_desc.description:
[email protected]e8077812012-02-03 03:41:461387 print "Description is empty; aborting."
[email protected]cc51cd02010-12-23 00:48:391388 return 1
[email protected]78936cb2013-04-11 00:17:521389 if CHANGE_ID not in change_desc.description:
1390 AddChangeIdToCommitMessage(options, args)
1391 if options.reviewers:
1392 change_desc.update_reviewers(options.reviewers)
[email protected]cc51cd02010-12-23 00:48:391393
[email protected]e8077812012-02-03 03:41:461394 receive_options = []
1395 cc = cl.GetCCList().split(',')
1396 if options.cc:
[email protected]eb52a5c2013-04-10 23:17:091397 cc.extend(options.cc)
[email protected]e8077812012-02-03 03:41:461398 cc = filter(None, cc)
1399 if cc:
1400 receive_options += ['--cc=' + email for email in cc]
[email protected]78936cb2013-04-11 00:17:521401 if change_desc.get_reviewers():
1402 receive_options.extend(
1403 '--reviewer=' + email for email in change_desc.get_reviewers())
[email protected]cc51cd02010-12-23 00:48:391404
[email protected]e8077812012-02-03 03:41:461405 git_command = ['push']
1406 if receive_options:
[email protected]19bbfa22012-02-03 16:18:111407 git_command.append('--receive-pack=git receive-pack %s' %
[email protected]e8077812012-02-03 03:41:461408 ' '.join(receive_options))
1409 git_command += [remote, 'HEAD:refs/for/' + branch]
1410 RunGit(git_command)
1411 # TODO(ukai): parse Change-Id: and set issue number?
1412 return 0
[email protected]970c5222011-03-12 00:32:241413
[email protected]cc51cd02010-12-23 00:48:391414
[email protected]e8077812012-02-03 03:41:461415def RietveldUpload(options, args, cl):
1416 """upload the patch to rietveld."""
[email protected]cc51cd02010-12-23 00:48:391417 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1418 upload_args.extend(['--server', cl.GetRietveldServer()])
[email protected]cc51cd02010-12-23 00:48:391419 if options.emulate_svn_auto_props:
1420 upload_args.append('--emulate_svn_auto_props')
[email protected]cc51cd02010-12-23 00:48:391421
1422 change_desc = None
1423
1424 if cl.GetIssue():
[email protected]420d3b82012-05-14 18:41:381425 if options.title:
1426 upload_args.extend(['--title', options.title])
[email protected]afadfca2013-05-29 14:15:531427 if options.message:
1428 upload_args.extend(['--message', options.message])
[email protected]52424302012-08-29 15:14:301429 upload_args.extend(['--issue', str(cl.GetIssue())])
[email protected]cc51cd02010-12-23 00:48:391430 print ("This branch is associated with issue %s. "
1431 "Adding patch to that issue." % cl.GetIssue())
1432 else:
[email protected]420d3b82012-05-14 18:41:381433 if options.title:
1434 upload_args.extend(['--title', options.title])
[email protected]43e34f02013-03-25 14:52:481435 message = options.title or options.message or CreateDescriptionFromLog(args)
[email protected]78936cb2013-04-11 00:17:521436 change_desc = ChangeDescription(message)
1437 if options.reviewers:
1438 change_desc.update_reviewers(options.reviewers)
[email protected]71e12a92012-02-14 02:34:151439 if not options.force:
[email protected]78936cb2013-04-11 00:17:521440 change_desc.prompt()
[email protected]970c5222011-03-12 00:32:241441
[email protected]78936cb2013-04-11 00:17:521442 if not change_desc.description:
[email protected]cc51cd02010-12-23 00:48:391443 print "Description is empty; aborting."
1444 return 1
[email protected]970c5222011-03-12 00:32:241445
[email protected]71e12a92012-02-14 02:34:151446 upload_args.extend(['--message', change_desc.description])
[email protected]78936cb2013-04-11 00:17:521447 if change_desc.get_reviewers():
1448 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
[email protected]a3353652011-11-30 14:26:571449 if options.send_mail:
[email protected]78936cb2013-04-11 00:17:521450 if not change_desc.get_reviewers():
[email protected]a3353652011-11-30 14:26:571451 DieWithError("Must specify reviewers to send email.")
1452 upload_args.append('--send_mail')
[email protected]99918ab2013-09-30 06:17:281453
1454 # We check this before applying rietveld.private assuming that in
1455 # rietveld.cc only addresses which we can send private CLs to are listed
1456 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1457 # --private is specified explicitly on the command line.
1458 if options.private:
1459 logging.warn('rietveld.cc is ignored since private flag is specified. '
1460 'You need to review and add them manually if necessary.')
1461 cc = cl.GetCCListWithoutDefault()
1462 else:
1463 cc = cl.GetCCList()
1464 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
[email protected]b2a7c332011-02-25 20:30:371465 if cc:
1466 upload_args.extend(['--cc', cc])
[email protected]cc51cd02010-12-23 00:48:391467
[email protected]c1737d02013-05-29 14:17:281468 if options.private or settings.GetDefaultPrivateFlag() == "True":
1469 upload_args.append('--private')
1470
[email protected]53937ba2012-10-02 18:20:431471 upload_args.extend(['--git_similarity', str(options.similarity)])
[email protected]79540052012-10-19 23:15:261472 if not options.find_copies:
1473 upload_args.extend(['--git_no_find_copies'])
[email protected]53937ba2012-10-02 18:20:431474
[email protected]cc51cd02010-12-23 00:48:391475 # Include the upstream repo's URL in the change -- this is useful for
1476 # projects that have their source spread across multiple repos.
[email protected]6b0051e2012-04-03 15:45:081477 remote_url = cl.GetGitBaseUrlFromConfig()
1478 if not remote_url:
1479 if settings.GetIsGitSvn():
1480 # URL is dependent on the current directory.
1481 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1482 if data:
1483 keys = dict(line.split(': ', 1) for line in data.splitlines()
1484 if ': ' in line)
1485 remote_url = keys.get('URL', None)
1486 else:
1487 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1488 remote_url = (cl.GetRemoteUrl() + '@'
1489 + cl.GetUpstreamBranch().split('/')[-1])
[email protected]cc51cd02010-12-23 00:48:391490 if remote_url:
1491 upload_args.extend(['--base_url', remote_url])
1492
1493 try:
[email protected]82880192012-11-26 15:41:571494 upload_args = ['upload'] + upload_args + args
1495 logging.info('upload.RealMain(%s)', upload_args)
1496 issue, patchset = upload.RealMain(upload_args)
[email protected]911fce12013-07-29 23:01:131497 issue = int(issue)
1498 patchset = int(patchset)
[email protected]9ce0dff2011-04-04 17:56:501499 except KeyboardInterrupt:
1500 sys.exit(1)
[email protected]cc51cd02010-12-23 00:48:391501 except:
1502 # If we got an exception after the user typed a description for their
1503 # change, back up the description before re-raising.
1504 if change_desc:
1505 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1506 print '\nGot exception while uploading -- saving description to %s\n' \
1507 % backup_path
1508 backup_file = open(backup_path, 'w')
[email protected]970c5222011-03-12 00:32:241509 backup_file.write(change_desc.description)
[email protected]cc51cd02010-12-23 00:48:391510 backup_file.close()
1511 raise
1512
1513 if not cl.GetIssue():
1514 cl.SetIssue(issue)
1515 cl.SetPatchset(patchset)
[email protected]27bb3872011-05-30 20:33:191516
1517 if options.use_commit_queue:
1518 cl.SetFlag('commit', '1')
[email protected]cc51cd02010-12-23 00:48:391519 return 0
1520
1521
[email protected]eb52a5c2013-04-10 23:17:091522def cleanup_list(l):
1523 """Fixes a list so that comma separated items are put as individual items.
1524
1525 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1526 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1527 """
1528 items = sum((i.split(',') for i in l), [])
1529 stripped_items = (i.strip() for i in items)
1530 return sorted(filter(None, stripped_items))
1531
1532
[email protected]0633fb42013-08-16 20:06:141533@subcommand.usage('[args to "git diff"]')
[email protected]e8077812012-02-03 03:41:461534def CMDupload(parser, args):
[email protected]d9c1b202013-07-24 23:52:111535 """Uploads the current changelist to codereview."""
[email protected]e8077812012-02-03 03:41:461536 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1537 help='bypass upload presubmit hook')
[email protected]b65c43c2013-06-10 22:04:491538 parser.add_option('--bypass-watchlists', action='store_true',
1539 dest='bypass_watchlists',
1540 help='bypass watchlists auto CC-ing reviewers')
[email protected]e8077812012-02-03 03:41:461541 parser.add_option('-f', action='store_true', dest='force',
1542 help="force yes to questions (don't prompt)")
[email protected]420d3b82012-05-14 18:41:381543 parser.add_option('-m', dest='message', help='message for patchset')
1544 parser.add_option('-t', dest='title', help='title for patchset')
[email protected]e8077812012-02-03 03:41:461545 parser.add_option('-r', '--reviewers',
[email protected]eb52a5c2013-04-10 23:17:091546 action='append', default=[],
[email protected]e8077812012-02-03 03:41:461547 help='reviewer email addresses')
1548 parser.add_option('--cc',
[email protected]eb52a5c2013-04-10 23:17:091549 action='append', default=[],
[email protected]e8077812012-02-03 03:41:461550 help='cc email addresses')
[email protected]36f47302013-04-05 01:08:311551 parser.add_option('-s', '--send-mail', action='store_true',
[email protected]e8077812012-02-03 03:41:461552 help='send email to reviewer immediately')
1553 parser.add_option("--emulate_svn_auto_props", action="store_true",
1554 dest="emulate_svn_auto_props",
1555 help="Emulate Subversion's auto properties feature.")
[email protected]e8077812012-02-03 03:41:461556 parser.add_option('-c', '--use-commit-queue', action='store_true',
1557 help='tell the commit queue to commit this patchset')
[email protected]c1737d02013-05-29 14:17:281558 parser.add_option('--private', action='store_true',
1559 help='set the review private (rietveld only)')
[email protected]8ef7ab22012-11-28 04:24:521560 parser.add_option('--target_branch',
1561 help='When uploading to gerrit, remote branch to '
1562 'use for CL. Default: master')
[email protected]53937ba2012-10-02 18:20:431563 add_git_similarity(parser)
[email protected]e8077812012-02-03 03:41:461564 (options, args) = parser.parse_args(args)
1565
[email protected]8ef7ab22012-11-28 04:24:521566 if options.target_branch and not settings.GetIsGerrit():
1567 parser.error('Use --target_branch for non gerrit repository.')
1568
[email protected]259e4682012-10-25 07:36:331569 if is_dirty_git_tree('upload'):
[email protected]e8077812012-02-03 03:41:461570 return 1
1571
[email protected]eb52a5c2013-04-10 23:17:091572 options.reviewers = cleanup_list(options.reviewers)
1573 options.cc = cleanup_list(options.cc)
1574
[email protected]e8077812012-02-03 03:41:461575 cl = Changelist()
1576 if args:
1577 # TODO(ukai): is it ok for gerrit case?
1578 base_branch = args[0]
1579 else:
[email protected]0f58fa82012-11-05 01:45:201580 # Default to diffing against common ancestor of upstream branch
1581 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
[email protected]5e07e062013-02-28 23:55:441582 args = [base_branch, 'HEAD']
[email protected]e8077812012-02-03 03:41:461583
[email protected]051ad0e2013-03-04 21:57:341584 # Apply watchlists on upload.
1585 change = cl.GetChange(base_branch, None)
1586 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1587 files = [f.LocalPath() for f in change.AffectedFiles()]
[email protected]b65c43c2013-06-10 22:04:491588 if not options.bypass_watchlists:
1589 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
[email protected]051ad0e2013-03-04 21:57:341590
[email protected]e8077812012-02-03 03:41:461591 if not options.bypass_hooks:
[email protected]051ad0e2013-03-04 21:57:341592 hook_results = cl.RunHook(committing=False,
[email protected]e8077812012-02-03 03:41:461593 may_prompt=not options.force,
1594 verbose=options.verbose,
[email protected]051ad0e2013-03-04 21:57:341595 change=change)
[email protected]e8077812012-02-03 03:41:461596 if not hook_results.should_continue():
1597 return 1
1598 if not options.reviewers and hook_results.reviewers:
[email protected]eb52a5c2013-04-10 23:17:091599 options.reviewers = hook_results.reviewers.split(',')
[email protected]e8077812012-02-03 03:41:461600
[email protected]5974d7a2013-04-02 20:50:371601 if cl.GetIssue():
[email protected]1033efd2013-07-23 23:25:091602 latest_patchset = cl.GetMostRecentPatchset()
[email protected]5974d7a2013-04-02 20:50:371603 local_patchset = cl.GetPatchset()
[email protected]07d149f2013-04-03 11:40:231604 if latest_patchset and local_patchset and local_patchset != latest_patchset:
[email protected]5974d7a2013-04-02 20:50:371605 print ('The last upload made from this repository was patchset #%d but '
1606 'the most recent patchset on the server is #%d.'
1607 % (local_patchset, latest_patchset))
[email protected]c7192782013-04-09 23:28:461608 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1609 'from another machine or branch the patch you\'re uploading now '
1610 'might not include those changes.')
[email protected]5974d7a2013-04-02 20:50:371611 ask_for_data('About to upload; enter to confirm.')
1612
[email protected]79540052012-10-19 23:15:261613 print_stats(options.similarity, options.find_copies, args)
[email protected]e8077812012-02-03 03:41:461614 if settings.GetIsGerrit():
1615 return GerritUpload(options, args, cl)
[email protected]caa16552013-03-18 20:45:051616 ret = RietveldUpload(options, args, cl)
1617 if not ret:
[email protected]4a6cd042013-04-12 15:40:421618 git_set_branch_value('last-upload-hash',
1619 RunGit(['rev-parse', 'HEAD']).strip())
[email protected]caa16552013-03-18 20:45:051620
1621 return ret
[email protected]e8077812012-02-03 03:41:461622
1623
[email protected]9bb85e22012-06-13 20:28:231624def IsSubmoduleMergeCommit(ref):
1625 # When submodules are added to the repo, we expect there to be a single
1626 # non-git-svn merge commit at remote HEAD with a signature comment.
1627 pattern = '^SVN changes up to revision [0-9]*$'
[email protected]e84b7542012-06-15 21:26:581628 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
[email protected]9bb85e22012-06-13 20:28:231629 return RunGit(cmd) != ''
1630
1631
[email protected]cc51cd02010-12-23 00:48:391632def SendUpstream(parser, args, cmd):
1633 """Common code for CmdPush and CmdDCommit
1634
1635 Squashed commit into a single.
1636 Updates changelog with metadata (e.g. pointer to review).
1637 Pushes/dcommits the code upstream.
1638 Updates review and closes.
1639 """
1640 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1641 help='bypass upload presubmit hook')
1642 parser.add_option('-m', dest='message',
1643 help="override review description")
1644 parser.add_option('-f', action='store_true', dest='force',
1645 help="force yes to questions (don't prompt)")
1646 parser.add_option('-c', dest='contributor',
1647 help="external contributor for patch (appended to " +
1648 "description and used as author for git). Should be " +
1649 "formatted as 'First Last <[email protected]>'")
[email protected]53937ba2012-10-02 18:20:431650 add_git_similarity(parser)
[email protected]cc51cd02010-12-23 00:48:391651 (options, args) = parser.parse_args(args)
1652 cl = Changelist()
1653
1654 if not args or cmd == 'push':
1655 # Default to merging against our best guess of the upstream branch.
1656 args = [cl.GetUpstreamBranch()]
1657
[email protected]13f623c2011-07-22 16:02:231658 if options.contributor:
1659 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1660 print "Please provide contibutor as 'First Last <[email protected]>'"
1661 return 1
1662
[email protected]cc51cd02010-12-23 00:48:391663 base_branch = args[0]
[email protected]9bb85e22012-06-13 20:28:231664 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
[email protected]cc51cd02010-12-23 00:48:391665
[email protected]259e4682012-10-25 07:36:331666 if is_dirty_git_tree(cmd):
[email protected]cc51cd02010-12-23 00:48:391667 return 1
1668
1669 # This rev-list syntax means "show all commits not in my branch that
1670 # are in base_branch".
1671 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1672 base_branch]).splitlines()
1673 if upstream_commits:
1674 print ('Base branch "%s" has %d commits '
1675 'not in this branch.' % (base_branch, len(upstream_commits)))
1676 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1677 return 1
1678
[email protected]9bb85e22012-06-13 20:28:231679 # This is the revision `svn dcommit` will commit on top of.
1680 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1681 '--pretty=format:%H'])
1682
[email protected]cc51cd02010-12-23 00:48:391683 if cmd == 'dcommit':
[email protected]9bb85e22012-06-13 20:28:231684 # If the base_head is a submodule merge commit, the first parent of the
1685 # base_head should be a git-svn commit, which is what we're interested in.
1686 base_svn_head = base_branch
1687 if base_has_submodules:
1688 base_svn_head += '^1'
1689
1690 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
[email protected]cc51cd02010-12-23 00:48:391691 if extra_commits:
1692 print ('This branch has %d additional commits not upstreamed yet.'
1693 % len(extra_commits.splitlines()))
1694 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1695 'before attempting to %s.' % (base_branch, cmd))
1696 return 1
1697
[email protected]0f58fa82012-11-05 01:45:201698 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
[email protected]b0a63912012-01-17 18:10:161699 if not options.bypass_hooks:
[email protected]13f623c2011-07-22 16:02:231700 author = None
1701 if options.contributor:
1702 author = re.search(r'\<(.*)\>', options.contributor).group(1)
[email protected]b0a63912012-01-17 18:10:161703 hook_results = cl.RunHook(
1704 committing=True,
[email protected]b0a63912012-01-17 18:10:161705 may_prompt=not options.force,
1706 verbose=options.verbose,
[email protected]051ad0e2013-03-04 21:57:341707 change=cl.GetChange(base_branch, author))
[email protected]b0a63912012-01-17 18:10:161708 if not hook_results.should_continue():
1709 return 1
[email protected]cc51cd02010-12-23 00:48:391710
1711 if cmd == 'dcommit':
1712 # Check the tree status if the tree status URL is set.
1713 status = GetTreeStatus()
1714 if 'closed' == status:
[email protected]b0a63912012-01-17 18:10:161715 print('The tree is closed. Please wait for it to reopen. Use '
1716 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
[email protected]cc51cd02010-12-23 00:48:391717 return 1
1718 elif 'unknown' == status:
[email protected]b0a63912012-01-17 18:10:161719 print('Unable to determine tree status. Please verify manually and '
1720 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
[email protected]ac637152012-01-16 14:19:541721 else:
1722 breakpad.SendStack(
1723 'GitClHooksBypassedCommit',
1724 'Issue %s/%s bypassed hook when committing' %
[email protected]2e72bb12012-01-17 15:18:351725 (cl.GetRietveldServer(), cl.GetIssue()),
1726 verbose=False)
[email protected]cc51cd02010-12-23 00:48:391727
[email protected]78936cb2013-04-11 00:17:521728 change_desc = ChangeDescription(options.message)
1729 if not change_desc.description and cl.GetIssue():
1730 change_desc = ChangeDescription(cl.GetDescription())
[email protected]cc51cd02010-12-23 00:48:391731
[email protected]78936cb2013-04-11 00:17:521732 if not change_desc.description:
[email protected]1a173982012-08-29 20:43:051733 if not cl.GetIssue() and options.bypass_hooks:
[email protected]78936cb2013-04-11 00:17:521734 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
[email protected]1a173982012-08-29 20:43:051735 else:
1736 print 'No description set.'
1737 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1738 return 1
[email protected]cc51cd02010-12-23 00:48:391739
[email protected]78936cb2013-04-11 00:17:521740 # Keep a separate copy for the commit message, because the commit message
1741 # contains the link to the Rietveld issue, while the Rietveld message contains
1742 # the commit viewvc url.
[email protected]e52678e2013-04-26 18:34:441743 # Keep a separate copy for the commit message.
1744 if cl.GetIssue():
[email protected]cf087782013-07-23 13:08:481745 change_desc.update_reviewers(cl.GetApprovingReviewers())
[email protected]e52678e2013-04-26 18:34:441746
[email protected]78936cb2013-04-11 00:17:521747 commit_desc = ChangeDescription(change_desc.description)
[email protected]cc73ad62011-07-06 17:39:261748 if cl.GetIssue():
[email protected]78936cb2013-04-11 00:17:521749 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
[email protected]cc51cd02010-12-23 00:48:391750 if options.contributor:
[email protected]78936cb2013-04-11 00:17:521751 commit_desc.append_footer('Patch from %s.' % options.contributor)
1752
[email protected]eec3ea32013-08-15 20:31:391753 print('Description:')
1754 print(commit_desc.description)
[email protected]cc51cd02010-12-23 00:48:391755
1756 branches = [base_branch, cl.GetBranchRef()]
1757 if not options.force:
[email protected]79540052012-10-19 23:15:261758 print_stats(options.similarity, options.find_copies, branches)
[email protected]90541732011-04-01 17:54:181759 ask_for_data('About to commit; enter to confirm.')
[email protected]cc51cd02010-12-23 00:48:391760
[email protected]9bb85e22012-06-13 20:28:231761 # We want to squash all this branch's commits into one commit with the proper
1762 # description. We do this by doing a "reset --soft" to the base branch (which
1763 # keeps the working copy the same), then dcommitting that. If origin/master
1764 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1765 # commit onto a branch based on the git-svn head.
[email protected]cc51cd02010-12-23 00:48:391766 MERGE_BRANCH = 'git-cl-commit'
[email protected]9bb85e22012-06-13 20:28:231767 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1768 # Delete the branches if they exist.
1769 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1770 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1771 result = RunGitWithCode(showref_cmd)
1772 if result[0] == 0:
1773 RunGit(['branch', '-D', branch])
[email protected]cc51cd02010-12-23 00:48:391774
1775 # We might be in a directory that's present in this branch but not in the
1776 # trunk. Move up to the top of the tree so that git commands that expect a
1777 # valid CWD won't fail after we check out the merge branch.
1778 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1779 if rel_base_path:
1780 os.chdir(rel_base_path)
1781
1782 # Stuff our change into the merge branch.
1783 # We wrap in a try...finally block so if anything goes wrong,
1784 # we clean up the branches.
[email protected]0ba7f962011-01-11 22:13:581785 retcode = -1
[email protected]cc51cd02010-12-23 00:48:391786 try:
[email protected]b4a75c42011-03-08 08:35:381787 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1788 RunGit(['reset', '--soft', base_branch])
[email protected]cc51cd02010-12-23 00:48:391789 if options.contributor:
[email protected]78936cb2013-04-11 00:17:521790 RunGit(
1791 [
1792 'commit', '--author', options.contributor,
1793 '-m', commit_desc.description,
1794 ])
[email protected]cc51cd02010-12-23 00:48:391795 else:
[email protected]78936cb2013-04-11 00:17:521796 RunGit(['commit', '-m', commit_desc.description])
[email protected]9bb85e22012-06-13 20:28:231797 if base_has_submodules:
1798 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1799 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1800 RunGit(['checkout', CHERRY_PICK_BRANCH])
1801 RunGit(['cherry-pick', cherry_pick_commit])
[email protected]cc51cd02010-12-23 00:48:391802 if cmd == 'push':
1803 # push the merge branch.
[email protected]0f58fa82012-11-05 01:45:201804 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
[email protected]cc51cd02010-12-23 00:48:391805 retcode, output = RunGitWithCode(
1806 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1807 logging.debug(output)
1808 else:
1809 # dcommit the merge branch.
[email protected]2e64fa12011-05-05 11:13:441810 retcode, output = RunGitWithCode(['svn', 'dcommit',
[email protected]53937ba2012-10-02 18:20:431811 '-C%s' % options.similarity,
[email protected]2e64fa12011-05-05 11:13:441812 '--no-rebase', '--rmdir'])
[email protected]cc51cd02010-12-23 00:48:391813 finally:
1814 # And then swap back to the original branch and clean up.
1815 RunGit(['checkout', '-q', cl.GetBranch()])
1816 RunGit(['branch', '-D', MERGE_BRANCH])
[email protected]9bb85e22012-06-13 20:28:231817 if base_has_submodules:
1818 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
[email protected]cc51cd02010-12-23 00:48:391819
1820 if cl.GetIssue():
1821 if cmd == 'dcommit' and 'Committed r' in output:
1822 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1823 elif cmd == 'push' and retcode == 0:
[email protected]df947ea2011-01-12 20:44:541824 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1825 for l in output.splitlines(False))
1826 match = filter(None, match)
1827 if len(match) != 1:
1828 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1829 output)
1830 revision = match[0].group(2)
[email protected]cc51cd02010-12-23 00:48:391831 else:
1832 return 1
1833 viewvc_url = settings.GetViewVCUrl()
1834 if viewvc_url and revision:
[email protected]78936cb2013-04-11 00:17:521835 change_desc.append_footer('Committed: ' + viewvc_url + revision)
[email protected]c22ea4b2012-10-09 22:42:001836 elif revision:
[email protected]78936cb2013-04-11 00:17:521837 change_desc.append_footer('Committed: ' + revision)
[email protected]cc51cd02010-12-23 00:48:391838 print ('Closing issue '
1839 '(you may be prompted for your codereview password)...')
[email protected]78936cb2013-04-11 00:17:521840 cl.UpdateDescription(change_desc.description)
[email protected]cc51cd02010-12-23 00:48:391841 cl.CloseIssue()
[email protected]1033efd2013-07-23 23:25:091842 props = cl.GetIssueProperties()
[email protected]34b5d822013-02-18 01:39:241843 patch_num = len(props['patchsets'])
[email protected]25a4ab42013-02-15 23:22:051844 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
[email protected]b85a3162013-01-26 01:11:131845 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1846 cl.RpcServer().add_comment(cl.GetIssue(), comment)
[email protected]1033efd2013-07-23 23:25:091847 cl.SetIssue(None)
[email protected]0ba7f962011-01-11 22:13:581848
1849 if retcode == 0:
1850 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1851 if os.path.isfile(hook):
[email protected]970c5222011-03-12 00:32:241852 RunCommand([hook, base_branch], error_ok=True)
[email protected]0ba7f962011-01-11 22:13:581853
[email protected]cc51cd02010-12-23 00:48:391854 return 0
1855
1856
[email protected]0633fb42013-08-16 20:06:141857@subcommand.usage('[upstream branch to apply against]')
[email protected]cc51cd02010-12-23 00:48:391858def CMDdcommit(parser, args):
[email protected]d9c1b202013-07-24 23:52:111859 """Commits the current changelist via git-svn."""
[email protected]cc51cd02010-12-23 00:48:391860 if not settings.GetIsGitSvn():
[email protected]cde3bb62011-01-20 01:16:141861 message = """This doesn't appear to be an SVN repository.
1862If your project has a git mirror with an upstream SVN master, you probably need
1863to run 'git svn init', see your project's git mirror documentation.
1864If your project has a true writeable upstream repository, you probably want
1865to run 'git cl push' instead.
1866Choose wisely, if you get this wrong, your commit might appear to succeed but
1867will instead be silently ignored."""
1868 print(message)
[email protected]90541732011-04-01 17:54:181869 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
[email protected]cc51cd02010-12-23 00:48:391870 return SendUpstream(parser, args, 'dcommit')
1871
1872
[email protected]0633fb42013-08-16 20:06:141873@subcommand.usage('[upstream branch to apply against]')
[email protected]cc51cd02010-12-23 00:48:391874def CMDpush(parser, args):
[email protected]d9c1b202013-07-24 23:52:111875 """Commits the current changelist via git."""
[email protected]cc51cd02010-12-23 00:48:391876 if settings.GetIsGitSvn():
1877 print('This appears to be an SVN repository.')
1878 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
[email protected]90541732011-04-01 17:54:181879 ask_for_data('[Press enter to push or ctrl-C to quit]')
[email protected]cc51cd02010-12-23 00:48:391880 return SendUpstream(parser, args, 'push')
1881
1882
[email protected]0633fb42013-08-16 20:06:141883@subcommand.usage('<patch url or issue id>')
[email protected]cc51cd02010-12-23 00:48:391884def CMDpatch(parser, args):
[email protected]e5e59002013-10-02 23:21:251885 """Patches in a code review."""
[email protected]cc51cd02010-12-23 00:48:391886 parser.add_option('-b', dest='newbranch',
1887 help='create a new branch off trunk for the patch')
[email protected]1ef44af2013-10-16 16:24:321888 parser.add_option('-f', '--force', action='store_true',
[email protected]cc51cd02010-12-23 00:48:391889 help='with -b, clobber any existing branch')
[email protected]1ef44af2013-10-16 16:24:321890 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1891 help='Change to the directory DIR immediately, '
1892 'before doing anything else.')
1893 parser.add_option('--reject', action='store_true',
[email protected]6a0b07c2013-07-10 01:29:191894 help='failed patches spew .rej files rather than '
1895 'attempting a 3-way merge')
[email protected]cc51cd02010-12-23 00:48:391896 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1897 help="don't commit after patch applies")
1898 (options, args) = parser.parse_args(args)
1899 if len(args) != 1:
1900 parser.print_help()
1901 return 1
[email protected]97ae58e2011-03-18 00:29:201902 issue_arg = args[0]
[email protected]cc51cd02010-12-23 00:48:391903
[email protected]27bb3872011-05-30 20:33:191904 # TODO(maruel): Use apply_issue.py
[email protected]e8077812012-02-03 03:41:461905 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
[email protected]27bb3872011-05-30 20:33:191906
[email protected]87b9bf02013-09-26 20:35:151907 if options.newbranch:
1908 if options.force:
1909 RunGit(['branch', '-D', options.newbranch],
1910 stderr=subprocess2.PIPE, error_ok=True)
1911 RunGit(['checkout', '-b', options.newbranch,
1912 Changelist().GetUpstreamBranch()])
1913
[email protected]1ef44af2013-10-16 16:24:321914 return PatchIssue(issue_arg, options.reject, options.nocommit,
1915 options.directory)
[email protected]87b9bf02013-09-26 20:35:151916
1917
[email protected]1ef44af2013-10-16 16:24:321918def PatchIssue(issue_arg, reject, nocommit, directory):
[email protected]87b9bf02013-09-26 20:35:151919 if type(issue_arg) is int or issue_arg.isdigit():
[email protected]cc51cd02010-12-23 00:48:391920 # Input is an issue id. Figure out the URL.
[email protected]52424302012-08-29 15:14:301921 issue = int(issue_arg)
[email protected]a26e0472013-07-24 10:25:011922 cl = Changelist(issue=issue)
[email protected]1033efd2013-07-23 23:25:091923 patchset = cl.GetMostRecentPatchset()
[email protected]0281f522012-09-14 13:37:591924 patch_data = cl.GetPatchSetDiff(issue, patchset)
[email protected]cc51cd02010-12-23 00:48:391925 else:
[email protected]eb5edbc2012-01-16 17:03:281926 # Assume it's a URL to the patch. Default to https.
1927 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
[email protected]0281f522012-09-14 13:37:591928 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
[email protected]e77ebbf2011-03-29 20:35:381929 if not match:
[email protected]cc51cd02010-12-23 00:48:391930 DieWithError('Must pass an issue ID or full URL for '
1931 '\'Download raw patch set\'')
[email protected]52424302012-08-29 15:14:301932 issue = int(match.group(1))
[email protected]0281f522012-09-14 13:37:591933 patchset = int(match.group(2))
[email protected]e77ebbf2011-03-29 20:35:381934 patch_data = urllib2.urlopen(issue_arg).read()
[email protected]cc51cd02010-12-23 00:48:391935
[email protected]cc51cd02010-12-23 00:48:391936 # Switch up to the top-level directory, if necessary, in preparation for
1937 # applying the patch.
1938 top = RunGit(['rev-parse', '--show-cdup']).strip()
1939 if top:
1940 os.chdir(top)
1941
[email protected]cc51cd02010-12-23 00:48:391942 # Git patches have a/ at the beginning of source paths. We strip that out
1943 # with a sed script rather than the -p flag to patch so we can feed either
1944 # Git or svn-style patches into the same apply command.
1945 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
[email protected]32f9f5e2011-09-14 13:41:471946 try:
1947 patch_data = subprocess2.check_output(
1948 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1949 except subprocess2.CalledProcessError:
[email protected]cc51cd02010-12-23 00:48:391950 DieWithError('Git patch mungling failed.')
1951 logging.info(patch_data)
[email protected]82b91cd2013-07-09 06:33:411952 env = os.environ.copy()
1953 # 'cat' is a magical git string that disables pagers on all platforms.
1954 env['GIT_PAGER'] = 'cat'
1955
[email protected]cc51cd02010-12-23 00:48:391956 # We use "git apply" to apply the patch instead of "patch" so that we can
1957 # pick up file adds.
1958 # The --index flag means: also insert into the index (so we catch adds).
[email protected]82b91cd2013-07-09 06:33:411959 cmd = ['git', 'apply', '--index', '-p0']
[email protected]1ef44af2013-10-16 16:24:321960 if directory:
1961 cmd.extend(('--directory', directory))
[email protected]87b9bf02013-09-26 20:35:151962 if reject:
[email protected]cc51cd02010-12-23 00:48:391963 cmd.append('--reject')
[email protected]6a0b07c2013-07-10 01:29:191964 elif IsGitVersionAtLeast('1.7.12'):
1965 cmd.append('--3way')
[email protected]32f9f5e2011-09-14 13:41:471966 try:
[email protected]82b91cd2013-07-09 06:33:411967 subprocess2.check_call(cmd, env=env,
1968 stdin=patch_data, stdout=subprocess2.VOID)
[email protected]32f9f5e2011-09-14 13:41:471969 except subprocess2.CalledProcessError:
[email protected]cc51cd02010-12-23 00:48:391970 DieWithError('Failed to apply the patch')
1971
1972 # If we had an issue, commit the current state and register the issue.
[email protected]87b9bf02013-09-26 20:35:151973 if not nocommit:
[email protected]cc51cd02010-12-23 00:48:391974 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1975 cl = Changelist()
1976 cl.SetIssue(issue)
[email protected]0281f522012-09-14 13:37:591977 cl.SetPatchset(patchset)
[email protected]98ca6622013-04-09 20:58:401978 print "Committed patch locally."
[email protected]cc51cd02010-12-23 00:48:391979 else:
1980 print "Patch applied to index."
1981 return 0
1982
1983
1984def CMDrebase(parser, args):
[email protected]d9c1b202013-07-24 23:52:111985 """Rebases current branch on top of svn repo."""
[email protected]cc51cd02010-12-23 00:48:391986 # Provide a wrapper for git svn rebase to help avoid accidental
1987 # git svn dcommit.
1988 # It's the only command that doesn't use parser at all since we just defer
1989 # execution to git-svn.
[email protected]82b91cd2013-07-09 06:33:411990 env = os.environ.copy()
1991 # 'cat' is a magical git string that disables pagers on all platforms.
1992 env['GIT_PAGER'] = 'cat'
1993
1994 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
[email protected]cc51cd02010-12-23 00:48:391995
1996
1997def GetTreeStatus():
1998 """Fetches the tree status and returns either 'open', 'closed',
1999 'unknown' or 'unset'."""
2000 url = settings.GetTreeStatusUrl(error_ok=True)
2001 if url:
2002 status = urllib2.urlopen(url).read().lower()
2003 if status.find('closed') != -1 or status == '0':
2004 return 'closed'
2005 elif status.find('open') != -1 or status == '1':
2006 return 'open'
2007 return 'unknown'
[email protected]cc51cd02010-12-23 00:48:392008 return 'unset'
2009
[email protected]970c5222011-03-12 00:32:242010
[email protected]cc51cd02010-12-23 00:48:392011def GetTreeStatusReason():
2012 """Fetches the tree status from a json url and returns the message
2013 with the reason for the tree to be opened or closed."""
[email protected]bf1a7ba2011-02-01 16:21:462014 url = settings.GetTreeStatusUrl()
2015 json_url = urlparse.urljoin(url, '/current?format=json')
[email protected]cc51cd02010-12-23 00:48:392016 connection = urllib2.urlopen(json_url)
2017 status = json.loads(connection.read())
2018 connection.close()
2019 return status['message']
2020
[email protected]970c5222011-03-12 00:32:242021
[email protected]cc51cd02010-12-23 00:48:392022def CMDtree(parser, args):
[email protected]d9c1b202013-07-24 23:52:112023 """Shows the status of the tree."""
[email protected]97ae58e2011-03-18 00:29:202024 _, args = parser.parse_args(args)
[email protected]cc51cd02010-12-23 00:48:392025 status = GetTreeStatus()
2026 if 'unset' == status:
2027 print 'You must configure your tree status URL by running "git cl config".'
2028 return 2
2029
2030 print "The tree is %s" % status
2031 print
2032 print GetTreeStatusReason()
2033 if status != 'open':
2034 return 1
2035 return 0
2036
2037
[email protected]15192402012-09-06 12:38:292038def CMDtry(parser, args):
2039 """Triggers a try job through Rietveld."""
2040 group = optparse.OptionGroup(parser, "Try job options")
2041 group.add_option(
2042 "-b", "--bot", action="append",
2043 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2044 "times to specify multiple builders. ex: "
2045 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2046 "the try server waterfall for the builders name and the tests "
2047 "available. Can also be used to specify gtest_filter, e.g. "
2048 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2049 group.add_option(
2050 "-r", "--revision",
2051 help="Revision to use for the try job; default: the "
2052 "revision will be determined by the try server; see "
2053 "its waterfall for more info")
2054 group.add_option(
2055 "-c", "--clobber", action="store_true", default=False,
2056 help="Force a clobber before building; e.g. don't do an "
2057 "incremental build")
2058 group.add_option(
2059 "--project",
2060 help="Override which project to use. Projects are defined "
2061 "server-side to define what default bot set to use")
2062 group.add_option(
2063 "-t", "--testfilter", action="append", default=[],
2064 help=("Apply a testfilter to all the selected builders. Unless the "
2065 "builders configurations are similar, use multiple "
2066 "--bot <builder>:<test> arguments."))
2067 group.add_option(
2068 "-n", "--name", help="Try job name; default to current branch name")
2069 parser.add_option_group(group)
2070 options, args = parser.parse_args(args)
2071
2072 if args:
2073 parser.error('Unknown arguments: %s' % args)
2074
2075 cl = Changelist()
2076 if not cl.GetIssue():
2077 parser.error('Need to upload first')
2078
2079 if not options.name:
2080 options.name = cl.GetBranch()
2081
2082 # Process --bot and --testfilter.
2083 if not options.bot:
2084 # Get try slaves from PRESUBMIT.py files if not specified.
[email protected]0f58fa82012-11-05 01:45:202085 change = cl.GetChange(
2086 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2087 None)
[email protected]15192402012-09-06 12:38:292088 options.bot = presubmit_support.DoGetTrySlaves(
2089 change,
2090 change.LocalPaths(),
2091 settings.GetRoot(),
2092 None,
2093 None,
2094 options.verbose,
2095 sys.stdout)
2096 if not options.bot:
2097 parser.error('No default try builder to try, use --bot')
2098
2099 builders_and_tests = {}
[email protected]43064fd2013-12-18 20:07:442100 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2101 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2102
2103 for bot in old_style:
[email protected]15192402012-09-06 12:38:292104 if ':' in bot:
2105 builder, tests = bot.split(':', 1)
2106 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2107 elif ',' in bot:
2108 parser.error('Specify one bot per --bot flag')
2109 else:
2110 builders_and_tests.setdefault(bot, []).append('defaulttests')
2111
[email protected]43064fd2013-12-18 20:07:442112 for bot, tests in new_style:
2113 builders_and_tests.setdefault(bot, []).extend(tests)
2114
[email protected]15192402012-09-06 12:38:292115 if options.testfilter:
2116 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2117 builders_and_tests = dict(
2118 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2119 if t != ['compile'])
2120
[email protected]f3b21232012-09-24 20:48:552121 if any('triggered' in b for b in builders_and_tests):
2122 print >> sys.stderr, (
2123 'ERROR You are trying to send a job to a triggered bot. This type of'
2124 ' bot requires an\ninitial job from a parent (usually a builder). '
2125 'Instead send your job to the parent.\n'
2126 'Bot list: %s' % builders_and_tests)
2127 return 1
2128
[email protected]36e420b2013-08-06 23:21:122129 patchset = cl.GetMostRecentPatchset()
2130 if patchset and patchset != cl.GetPatchset():
2131 print(
2132 '\nWARNING Mismatch between local config and server. Did a previous '
2133 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2134 'Continuing using\npatchset %s.\n' % patchset)
[email protected]d246c972013-12-21 22:47:382135 try:
2136 cl.RpcServer().trigger_try_jobs(
2137 cl.GetIssue(), patchset, options.name, options.clobber,
2138 options.revision, builders_and_tests)
2139 except urllib2.HTTPError, e:
2140 if e.code == 404:
2141 print('404 from rietveld; '
2142 'did you mean to use "git try" instead of "git cl try"?')
2143 return 1
[email protected]072d94b2012-09-20 19:20:082144 print('Tried jobs on:')
2145 length = max(len(builder) for builder in builders_and_tests)
2146 for builder in sorted(builders_and_tests):
2147 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
[email protected]15192402012-09-06 12:38:292148 return 0
2149
2150
[email protected]0633fb42013-08-16 20:06:142151@subcommand.usage('[new upstream branch]')
[email protected]cc51cd02010-12-23 00:48:392152def CMDupstream(parser, args):
[email protected]d9c1b202013-07-24 23:52:112153 """Prints or sets the name of the upstream branch, if any."""
[email protected]97ae58e2011-03-18 00:29:202154 _, args = parser.parse_args(args)
[email protected]ac0ba332012-08-09 23:42:532155 if len(args) > 1:
[email protected]27bb3872011-05-30 20:33:192156 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]ac0ba332012-08-09 23:42:532157
[email protected]cc51cd02010-12-23 00:48:392158 cl = Changelist()
[email protected]ac0ba332012-08-09 23:42:532159 if args:
2160 # One arg means set upstream branch.
2161 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2162 cl = Changelist()
2163 print "Upstream branch set to " + cl.GetUpstreamBranch()
2164 else:
2165 print cl.GetUpstreamBranch()
[email protected]cc51cd02010-12-23 00:48:392166 return 0
2167
2168
[email protected]00858c82013-12-02 23:08:032169def CMDweb(parser, args):
2170 """Opens the current CL in the web browser."""
2171 _, args = parser.parse_args(args)
2172 if args:
2173 parser.error('Unrecognized args: %s' % ' '.join(args))
2174
2175 issue_url = Changelist().GetIssueURL()
2176 if not issue_url:
2177 print >> sys.stderr, 'ERROR No issue to open'
2178 return 1
2179
2180 webbrowser.open(issue_url)
2181 return 0
2182
2183
[email protected]27bb3872011-05-30 20:33:192184def CMDset_commit(parser, args):
[email protected]d9c1b202013-07-24 23:52:112185 """Sets the commit bit to trigger the Commit Queue."""
[email protected]27bb3872011-05-30 20:33:192186 _, args = parser.parse_args(args)
2187 if args:
2188 parser.error('Unrecognized args: %s' % ' '.join(args))
2189 cl = Changelist()
2190 cl.SetFlag('commit', '1')
2191 return 0
2192
2193
[email protected]411034a2013-02-26 15:12:012194def CMDset_close(parser, args):
[email protected]d9c1b202013-07-24 23:52:112195 """Closes the issue."""
[email protected]411034a2013-02-26 15:12:012196 _, args = parser.parse_args(args)
2197 if args:
2198 parser.error('Unrecognized args: %s' % ' '.join(args))
2199 cl = Changelist()
2200 # Ensure there actually is an issue to close.
2201 cl.GetDescription()
2202 cl.CloseIssue()
2203 return 0
2204
2205
[email protected]87b9bf02013-09-26 20:35:152206def CMDdiff(parser, args):
2207 """shows differences between local tree and last upload."""
2208 cl = Changelist()
[email protected]78dc9842013-11-25 18:43:442209 issue = cl.GetIssue()
[email protected]87b9bf02013-09-26 20:35:152210 branch = cl.GetBranch()
[email protected]78dc9842013-11-25 18:43:442211 if not issue:
2212 DieWithError('No issue found for current branch (%s)' % branch)
[email protected]87b9bf02013-09-26 20:35:152213 TMP_BRANCH = 'git-cl-diff'
2214 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2215
2216 # Create a new branch based on the merge-base
2217 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2218 try:
2219 # Patch in the latest changes from rietveld.
[email protected]78dc9842013-11-25 18:43:442220 rtn = PatchIssue(issue, False, False, None)
[email protected]87b9bf02013-09-26 20:35:152221 if rtn != 0:
2222 return rtn
2223
2224 # Switch back to starting brand and diff against the temporary
2225 # branch containing the latest rietveld patch.
2226 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2227 finally:
2228 RunGit(['checkout', '-q', branch])
2229 RunGit(['branch', '-D', TMP_BRANCH])
2230
2231 return 0
2232
2233
[email protected]faf3fdf2013-09-20 02:11:482234def CMDowners(parser, args):
2235 """interactively find the owners for reviewing"""
2236 parser.add_option(
2237 '--no-color',
2238 action='store_true',
2239 help='Use this option to disable color output')
2240 options, args = parser.parse_args(args)
2241
2242 author = RunGit(['config', 'user.email']).strip() or None
2243
2244 cl = Changelist()
2245
2246 if args:
2247 if len(args) > 1:
2248 parser.error('Unknown args')
2249 base_branch = args[0]
2250 else:
2251 # Default to diffing against the common ancestor of the upstream branch.
2252 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2253
2254 change = cl.GetChange(base_branch, None)
2255 return owners_finder.OwnersFinder(
2256 [f.LocalPath() for f in
2257 cl.GetChange(base_branch, None).AffectedFiles()],
2258 change.RepositoryRoot(), author,
2259 fopen=file, os_path=os.path, glob=glob.glob,
2260 disable_color=options.no_color).run()
2261
2262
[email protected]fab8f822013-05-06 17:43:092263def CMDformat(parser, args):
[email protected]d9c1b202013-07-24 23:52:112264 """Runs clang-format on the diff."""
[email protected]fab8f822013-05-06 17:43:092265 CLANG_EXTS = ['.cc', '.cpp', '.h']
2266 parser.add_option('--full', action='store_true', default=False)
2267 opts, args = parser.parse_args(args)
2268 if args:
2269 parser.error('Unrecognized args: %s' % ' '.join(args))
2270
[email protected]ff7a1fb2013-12-10 19:21:412271 # git diff generates paths against the root of the repository. Change
2272 # to that directory so clang-format can find files even within subdirs.
2273 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2274 if rel_base_path:
2275 os.chdir(rel_base_path)
2276
[email protected]29e47272013-05-17 17:01:462277 # Generate diff for the current branch's changes.
[email protected]90d30c62013-05-29 16:09:492278 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
[email protected]fab8f822013-05-06 17:43:092279 if opts.full:
[email protected]29e47272013-05-17 17:01:462280 # Only list the names of modified files.
2281 diff_cmd.append('--name-only')
2282 else:
2283 # Only generate context-less patches.
2284 diff_cmd.append('-U0')
2285
2286 # Grab the merge-base commit, i.e. the upstream commit of the current
2287 # branch when it was created or the last time it was rebased. This is
2288 # to cover the case where the user may have called "git fetch origin",
2289 # moving the origin branch to a newer commit, but hasn't rebased yet.
2290 upstream_commit = None
2291 cl = Changelist()
2292 upstream_branch = cl.GetUpstreamBranch()
2293 if upstream_branch:
2294 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2295 upstream_commit = upstream_commit.strip()
2296
2297 if not upstream_commit:
2298 DieWithError('Could not find base commit for this branch. '
2299 'Are you in detached state?')
2300
2301 diff_cmd.append(upstream_commit)
2302
2303 # Handle source file filtering.
2304 diff_cmd.append('--')
2305 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2306 diff_output = RunGit(diff_cmd)
2307
[email protected]c3b3dc02013-08-05 23:09:492308 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2309
[email protected]29e47272013-05-17 17:01:462310 if opts.full:
2311 # diff_output is a list of files to send to clang-format.
2312 files = diff_output.splitlines()
[email protected]fab8f822013-05-06 17:43:092313 if not files:
2314 print "Nothing to format."
2315 return 0
[email protected]c3b3dc02013-08-05 23:09:492316 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2317 cwd=top_dir)
[email protected]fab8f822013-05-06 17:43:092318 else:
[email protected]29e47272013-05-17 17:01:462319 # diff_output is a patch to send to clang-format-diff.py
[email protected]fab8f822013-05-06 17:43:092320 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2321 'clang-format-diff.py')
2322 if not os.path.exists(cfd_path):
[email protected]29e47272013-05-17 17:01:462323 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
[email protected]16d2ad62013-09-27 14:54:042324 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
[email protected]d6ddc1c2013-10-25 15:36:322325
2326 # Newer versions of clang-format-diff.py require an explicit -i flag
2327 # to apply the edits to files, otherwise it just displays a diff.
2328 # Probe the usage string to verify if this is needed.
2329 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2330 if '[-i]' in help_text:
2331 cmd.append('-i')
2332
[email protected]c3b3dc02013-08-05 23:09:492333 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
[email protected]fab8f822013-05-06 17:43:092334
2335 return 0
2336
2337
[email protected]d9c1b202013-07-24 23:52:112338class OptionParser(optparse.OptionParser):
2339 """Creates the option parse and add --verbose support."""
2340 def __init__(self, *args, **kwargs):
[email protected]0633fb42013-08-16 20:06:142341 optparse.OptionParser.__init__(
2342 self, *args, prog='git cl', version=__version__, **kwargs)
[email protected]d9c1b202013-07-24 23:52:112343 self.add_option(
2344 '-v', '--verbose', action='count', default=0,
2345 help='Use 2 times for more debugging info')
2346
2347 def parse_args(self, args=None, values=None):
2348 options, args = optparse.OptionParser.parse_args(self, args, values)
2349 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2350 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2351 return options, args
2352
[email protected]d9c1b202013-07-24 23:52:112353
[email protected]cc51cd02010-12-23 00:48:392354def main(argv):
[email protected]82798cb2012-02-23 18:16:122355 if sys.hexversion < 0x02060000:
2356 print >> sys.stderr, (
2357 '\nYour python version %s is unsupported, please upgrade.\n' %
2358 sys.version.split(' ', 1)[0])
2359 return 2
[email protected]2e23ce32013-05-07 12:42:282360
[email protected]ddd59412011-11-30 14:20:382361 # Reload settings.
2362 global settings
2363 settings = Settings()
2364
[email protected]39c0b222013-08-17 16:57:012365 colorize_CMDstatus_doc()
[email protected]0633fb42013-08-16 20:06:142366 dispatcher = subcommand.CommandDispatcher(__name__)
2367 try:
2368 return dispatcher.execute(OptionParser(), argv)
2369 except urllib2.HTTPError, e:
2370 if e.code != 500:
2371 raise
2372 DieWithError(
2373 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2374 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
[email protected]cc51cd02010-12-23 00:48:392375
2376
2377if __name__ == '__main__':
[email protected]2e23ce32013-05-07 12:42:282378 # These affect sys.stdout so do it outside of main() to simplify mocks in
2379 # unit testing.
[email protected]6f09cd92011-04-01 16:38:122380 fix_encoding.fix_encoding()
[email protected]2e23ce32013-05-07 12:42:282381 colorama.init()
[email protected]cc51cd02010-12-23 00:48:392382 sys.exit(main(sys.argv[1:]))