blob: d01c354169f691a96da77c650b10abea18b5edf0 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000035import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000036import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000038import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000040import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000041import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000043import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000047DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000048POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000050GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000051CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000053# Shortcut since it quickly becomes redundant.
54Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000055
maruel@chromium.orgddd59412011-11-30 14:20:38 +000056# Initialized in main()
57settings = None
58
59
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000061 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062 sys.exit(1)
63
64
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000067 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000068 except subprocess2.CalledProcessError as e:
69 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 'Command "%s" failed.\n%s' % (
73 ' '.join(args), error_message or e.stdout or ''))
74 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
76
77def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000079 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000084 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000085 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,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000090 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000091 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, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096
97
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000098def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000099 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000100 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000101 return (version.startswith(prefix) and
102 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000103
104
maruel@chromium.org90541732011-04-01 17:54:18 +0000105def ask_for_data(prompt):
106 try:
107 return raw_input(prompt)
108 except KeyboardInterrupt:
109 # Hide the exception.
110 sys.exit(1)
111
112
iannucci@chromium.org79540052012-10-19 23:15:26 +0000113def git_set_branch_value(key, value):
114 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000115 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)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000123
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
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137def add_git_similarity(parser):
138 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000140 help='Sets the percentage that a pair of files need to match in order to'
141 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 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.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000148
149 old_parser_args = parser.parse_args
150 def Parse(args):
151 options, args = old_parser_args(args)
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156 print('Note: Saving similarity of %d%% in git config.'
157 % options.similarity)
158 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160 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))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000167
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
ukai@chromium.org259e4682012-10-25 07:36:33 +0000175def 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
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000188
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000189def 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
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000230
iannucci@chromium.org79540052012-10-19 23:15:26 +0000231def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000232 """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']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000239 # 'cat' is a magical git string that disables pagers on all platforms.
240 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241
242 if find_copies:
243 similarity_options = ['--find-copies-harder', '-l100000',
244 '-C%s' % similarity]
245 else:
246 similarity_options = ['-M%s' % similarity]
247
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000249 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000250 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252
253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000254class 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
ukai@chromium.orge8077812012-02-03 03:41:46 +0000264 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000265 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266
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)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000273 self.updated = True
274 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 self.updated = True
276
277 def GetDefaultServerUrl(self, error_ok=False):
278 if not self.default_server:
279 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000280 self.default_server = gclient_utils.UpgradeToHttps(
281 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282 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".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000287 self.default_server = gclient_utils.UpgradeToHttps(
288 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289 return self.default_server
290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000291 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(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000301 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 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:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000311 # 1) iterate through our branch history and find the svn URL.
312 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000313
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
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000317 env = os.environ.copy()
318 # 'cat' is a magical git string that disables pagers on all platforms.
319 env['GIT_PAGER'] = 'cat'
320
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000321 # 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.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000324 cmd = ['git', 'log', '-100', '--pretty=medium']
325 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000326 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000327 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000334 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)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000340 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000341 remote = match.group(1)
342 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000343 rewrite_root = RunGit(
344 ['config', 'svn-remote.%s.rewriteRoot' % remote],
345 error_ok=True).strip()
346 if rewrite_root:
347 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000348 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000349 ['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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000369
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:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000387 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000388 return self.viewvc_url
389
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000390 def GetDefaultCCList(self):
391 return self._GetConfig('rietveld.cc', error_ok=True)
392
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000393 def GetDefaultPrivateFlag(self):
394 return self._GetConfig('rietveld.private', error_ok=True)
395
ukai@chromium.orge8077812012-02-03 03:41:46 +0000396 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
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000402 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000408 def _GetConfig(self, param, **kwargs):
409 self.LazyUpdateIfNeeded()
410 return RunGit(['config', param], **kwargs).strip()
411
412
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000413def ShortBranchName(branch):
414 """Convert a name like 'refs/heads/foo' to just 'foo'."""
415 return branch.replace('refs/heads/', '')
416
417
418class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000419 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000420 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000421 global settings
422 if not settings:
423 # Happens when git_cl.py is used as a utility library.
424 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425 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
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000433 self.lookedup_issue = False
434 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000435 self.has_description = False
436 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000437 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000438 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000439 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000440 self.cc = None
441 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000442 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000443 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000444
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:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000451 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000452 more_cc = ','.join(self.watchers)
453 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
454 return self.cc
455
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000456 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
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000462 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000467
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
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000480 @staticmethod
481 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000482 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000483 e.g. 'origin', 'refs/heads/master'
484 """
485 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000486 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:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000491 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
492 error_ok=True).strip()
493 if upstream_branch:
494 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000495 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000496 # Fall back on trying a git-svn upstream branch.
497 if settings.GetIsGitSvn():
498 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000499 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000500 # 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.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000513Either 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:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000522 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000523 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
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000528 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000529 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000530 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:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000539 remotes = RunGit(['remote'], error_ok=True).split()
540 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000541 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000542 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000543 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000544 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)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000555 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))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000560 return self._remote
561
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000562 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
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000597 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()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 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 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000610 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
612
613 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000614 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000615 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000617 self.issue = int(issue) or None if issue else None
618 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 return self.issue
620
621 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000622 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()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 return self.rietveld_server
631
632 def GetIssueURL(self):
633 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000634 if not self.GetIssue():
635 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000636 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
637
638 def GetDescription(self, pretty=False):
639 if not self.has_description:
640 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000641 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000642 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(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000657 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658 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):
maruel@chromium.org52424302012-08-29 15:14:30 +0000666 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000667 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668 patchset = RunGit(['config', self._PatchsetSetting()],
669 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000670 self.patchset = int(patchset) or None if patchset else None
671 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672 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)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000678 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000679 else:
680 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000681 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000682 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000684 def GetMostRecentPatchset(self):
685 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000686
687 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000688 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000689 '/download/issue%s_%s.diff' % (issue, patchset))
690
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000691 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
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000700 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000701 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000702
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703 def SetIssue(self, issue):
704 """Set this branch's issue. If issue=0, clears the issue."""
705 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000706 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707 RunGit(['config', self._IssueSetting(), str(issue)])
708 if self.rietveld_server:
709 RunGit(['config', self._RietveldServer(), self.rietveld_server])
710 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000711 current_issue = self.GetIssue()
712 if current_issue:
713 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000714 self.issue = None
715 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000717 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000718 if not self.GitSanityChecks(upstream_branch):
719 DieWithError('\nGit sanity check failure')
720
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000721 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()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000726 if not root:
727 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000728 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000729
730 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000731 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000732 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000733 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000734 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000735 except subprocess2.CalledProcessError:
736 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000737 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000738 '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()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000743
maruel@chromium.org52424302012-08-29 15:14:30 +0000744 issue = self.GetIssue()
745 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000746 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.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000752 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000753 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000754 '%s...' % (upstream_branch)],
755 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000756
757 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000758 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000759 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000760 name,
761 description,
762 absroot,
763 files,
764 issue,
765 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000766 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000767
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000768 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000769 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000770
771 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000772 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000773 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000774 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000775 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000776 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
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000781 def UpdateDescription(self, description):
782 self.description = description
783 return self.RpcServer().update_description(
784 self.GetIssue(), self.description)
785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000787 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000788 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000790 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(
maruel@chromium.org52424302012-08-29 15:14:30 +0000796 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000797 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000806 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 """Returns an upload.RpcServer() to access this review's rietveld instance.
808 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000809 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000810 self._rpc_server = rietveld.CachingRietveld(
811 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000812 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813
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."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000829 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830 server = settings.GetDefaultServerUrl(error_ok=True)
831 prompt = 'Rietveld server (host[:port])'
832 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000833 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 if not server and not newserver:
835 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000836 if newserver:
837 newserver = gclient_utils.UpgradeToHttps(newserver)
838 if newserver != server:
839 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000841 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 prompt = caption
843 if initial:
844 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000845 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 if new_val == 'x':
847 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000848 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])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000854 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000855 SetProperty(settings.GetDefaultPrivateFlag(),
856 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000858 'tree-status-url', False)
859 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860
861 # TODO: configure a default branch to diff against, rather than this
862 # svn-based hackery.
863
864
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000865class ChangeDescription(object):
866 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000867 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000868 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000869
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000870 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000871 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000872
agable@chromium.org42c20792013-09-12 17:34:49 +0000873 @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
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000887
888 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000889 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000890 assert isinstance(reviewers, list), reviewers
891 if not reviewers:
892 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000893 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000894
agable@chromium.org42c20792013-09-12 17:34:49 +0000895 # 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)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000901
agable@chromium.org42c20792013-09-12 17:34:49 +0000902 # 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)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000926 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000927 if new_r_line:
928 self.append_footer(new_r_line)
929 if new_tbr_line:
930 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000931
932 def prompt(self):
933 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000934 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.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000938 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000939 '--------------------',
940 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000941
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 regexp = re.compile(self.BUG_LINE)
943 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000944 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000945 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000946 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000947 if not content:
948 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000949 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000950
951 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000952 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
953 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000954 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000955 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000956
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000957 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000958 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)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000965
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000966 def get_reviewers(self):
967 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000968 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]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000970 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000971
972
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000973def 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989def 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."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001011 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 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)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001024 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1026 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1027
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001028 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001029 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001030
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001039def 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
ukai@chromium.org712d6102013-11-27 00:52:58 +00001046def hasSheBang(fname):
1047 """Checks fname is a #! script."""
1048 with open(fname) as f:
1049 return f.read(2).startswith('#!')
1050
1051
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001052def 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
ukai@chromium.org712d6102013-11-27 00:52:58 +00001060 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001061 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
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001066 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001067 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001068 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))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001073 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)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001077 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)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001081
1082
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001083@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001085 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001087 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088 if len(args) == 0:
1089 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001090 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 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))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001099 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 return 0
1101
1102
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001103def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001104 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001105 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001119 """Show status of changelists.
1120
1121 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001122 - 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
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001128
1129 Also see 'git cl comments'.
1130 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 parser.add_option('--field',
1132 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001133 parser.add_option('-f', '--fast', action='store_true',
1134 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001136 if args:
1137 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001140 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 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
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001155 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:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001166 # 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):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001174 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001175 c = Changelist(branchref=b)
1176 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001177 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
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001194 elif props.get('commit'):
1195 # Issue is in the commit queue.
1196 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001197 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))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208
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)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001217 url = c.GetIssueURL()
1218 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001219
1220 tmp = {}
1221 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001222 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001223 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001224 b, i, color = output.get()
1225 tmp[b] = (i, color)
1226 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001227 reset = Fore.RESET
1228 if not sys.stdout.isatty():
1229 color = ''
1230 reset = ''
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001231 print ' %*s: %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001232 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001234 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)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 return 0
1245
1246
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001247def 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
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001263@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001265 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266
1267 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001268 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001269 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270
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)
maruel@chromium.org52424302012-08-29 15:14:30 +00001279 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 return 0
1281
1282
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001283def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001284 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001285 (_, args) = parser.parse_args(args)
1286 if args:
1287 parser.error('Unsupported argument: %s' % args)
1288
1289 cl = Changelist()
1290 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001291 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001292 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001293 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)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001304 if message['text'].strip():
1305 print '\n'.join(' ' + l for l in message['text'].splitlines())
1306 return 0
1307
1308
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001309def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001310 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001311 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320def 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!
maruel@chromium.org373af802012-05-25 21:07:33 +00001331 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
1333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001335 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001336 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001338 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001339 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 (options, args) = parser.parse_args(args)
1341
ukai@chromium.org259e4682012-10-25 07:36:33 +00001342 if not options.force and is_dirty_git_tree('presubmit'):
1343 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 return 1
1345
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001346 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 if args:
1348 base_branch = args[0]
1349 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001350 # Default to diffing against the common ancestor of the upstream branch.
1351 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001353 cl.RunHook(
1354 committing=not options.upload,
1355 may_prompt=False,
1356 verbose=options.verbose,
1357 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001358 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359
1360
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001361def 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
ukai@chromium.orge8077812012-02-03 03:41:46 +00001375def 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001384 change_desc = ChangeDescription(
1385 options.message or CreateDescriptionFromLog(args))
1386 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001387 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001389 if CHANGE_ID not in change_desc.description:
1390 AddChangeIdToCommitMessage(options, args)
1391 if options.reviewers:
1392 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
ukai@chromium.orge8077812012-02-03 03:41:46 +00001394 receive_options = []
1395 cc = cl.GetCCList().split(',')
1396 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001397 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001398 cc = filter(None, cc)
1399 if cc:
1400 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001401 if change_desc.get_reviewers():
1402 receive_options.extend(
1403 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404
ukai@chromium.orge8077812012-02-03 03:41:46 +00001405 git_command = ['push']
1406 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001407 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001408 ' '.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
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001413
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
ukai@chromium.orge8077812012-02-03 03:41:46 +00001415def RietveldUpload(options, args, cl):
1416 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1418 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 if options.emulate_svn_auto_props:
1420 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
1422 change_desc = None
1423
1424 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001425 if options.title:
1426 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001427 if options.message:
1428 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001429 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 print ("This branch is associated with issue %s. "
1431 "Adding patch to that issue." % cl.GetIssue())
1432 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001433 if options.title:
1434 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001435 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001436 change_desc = ChangeDescription(message)
1437 if options.reviewers:
1438 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001439 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001440 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001441
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001442 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 print "Description is empty; aborting."
1444 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001445
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001446 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001447 if change_desc.get_reviewers():
1448 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001449 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001450 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001451 DieWithError("Must specify reviewers to send email.")
1452 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001453
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))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001465 if cc:
1466 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001468 if options.private or settings.GetDefaultPrivateFlag() == "True":
1469 upload_args.append('--private')
1470
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001471 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001472 if not options.find_copies:
1473 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001474
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 # Include the upstream repo's URL in the change -- this is useful for
1476 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001477 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])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490 if remote_url:
1491 upload_args.extend(['--base_url', remote_url])
1492
1493 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001494 upload_args = ['upload'] + upload_args + args
1495 logging.info('upload.RealMain(%s)', upload_args)
1496 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001497 issue = int(issue)
1498 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001499 except KeyboardInterrupt:
1500 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 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')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001509 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510 backup_file.close()
1511 raise
1512
1513 if not cl.GetIssue():
1514 cl.SetIssue(issue)
1515 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001516
1517 if options.use_commit_queue:
1518 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519 return 0
1520
1521
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001522def 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
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001533@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001534def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001535 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001536 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1537 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001538 parser.add_option('--bypass-watchlists', action='store_true',
1539 dest='bypass_watchlists',
1540 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541 parser.add_option('-f', action='store_true', dest='force',
1542 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001543 parser.add_option('-m', dest='message', help='message for patchset')
1544 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001545 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001546 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547 help='reviewer email addresses')
1548 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001549 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001550 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001551 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001552 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.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001556 parser.add_option('-c', '--use-commit-queue', action='store_true',
1557 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001558 parser.add_option('--private', action='store_true',
1559 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001560 parser.add_option('--target_branch',
1561 help='When uploading to gerrit, remote branch to '
1562 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001563 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001564 (options, args) = parser.parse_args(args)
1565
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001566 if options.target_branch and not settings.GetIsGerrit():
1567 parser.error('Use --target_branch for non gerrit repository.')
1568
ukai@chromium.org259e4682012-10-25 07:36:33 +00001569 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001570 return 1
1571
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001572 options.reviewers = cleanup_list(options.reviewers)
1573 options.cc = cleanup_list(options.cc)
1574
ukai@chromium.orge8077812012-02-03 03:41:46 +00001575 cl = Changelist()
1576 if args:
1577 # TODO(ukai): is it ok for gerrit case?
1578 base_branch = args[0]
1579 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001580 # Default to diffing against common ancestor of upstream branch
1581 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001582 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001583
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001584 # 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()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001588 if not options.bypass_watchlists:
1589 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001590
ukai@chromium.orge8077812012-02-03 03:41:46 +00001591 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001592 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001593 may_prompt=not options.force,
1594 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001595 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001596 if not hook_results.should_continue():
1597 return 1
1598 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001599 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001600
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001601 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001602 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001603 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001604 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001605 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))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001608 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.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001611 ask_for_data('About to upload; enter to confirm.')
1612
iannucci@chromium.org79540052012-10-19 23:15:26 +00001613 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001614 if settings.GetIsGerrit():
1615 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001616 ret = RietveldUpload(options, args, cl)
1617 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001618 git_set_branch_value('last-upload-hash',
1619 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001620
1621 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001622
1623
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001624def 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]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001628 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001629 return RunGit(cmd) != ''
1630
1631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632def 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@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001650 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 (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
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001658 if options.contributor:
1659 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1660 print "Please provide contibutor as 'First Last <email@example.com>'"
1661 return 1
1662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001664 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001665
ukai@chromium.org259e4682012-10-25 07:36:33 +00001666 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001667 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
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001679 # 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001683 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001684 # 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])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691 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
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001698 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001699 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001700 author = None
1701 if options.contributor:
1702 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001703 hook_results = cl.RunHook(
1704 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001705 may_prompt=not options.force,
1706 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001707 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001708 if not hook_results.should_continue():
1709 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710
1711 if cmd == 'dcommit':
1712 # Check the tree status if the tree status URL is set.
1713 status = GetTreeStatus()
1714 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001715 print('The tree is closed. Please wait for it to reopen. Use '
1716 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717 return 1
1718 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001719 print('Unable to determine tree status. Please verify manually and '
1720 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001721 else:
1722 breakpad.SendStack(
1723 'GitClHooksBypassedCommit',
1724 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001725 (cl.GetRietveldServer(), cl.GetIssue()),
1726 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001727
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001728 change_desc = ChangeDescription(options.message)
1729 if not change_desc.description and cl.GetIssue():
1730 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001732 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001733 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001734 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001735 else:
1736 print 'No description set.'
1737 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1738 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001740 # 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.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001743 # Keep a separate copy for the commit message.
1744 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001745 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001746
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001747 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001748 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001749 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001751 commit_desc.append_footer('Patch from %s.' % options.contributor)
1752
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001753 print('Description:')
1754 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755
1756 branches = [base_branch, cl.GetBranchRef()]
1757 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001758 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001759 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001761 # 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.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001767 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])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001774
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.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001785 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001787 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1788 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001790 RunGit(
1791 [
1792 'commit', '--author', options.contributor,
1793 '-m', commit_desc.description,
1794 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001796 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001797 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])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001802 if cmd == 'push':
1803 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001804 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805 retcode, output = RunGitWithCode(
1806 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1807 logging.debug(output)
1808 else:
1809 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001810 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001811 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001812 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001813 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])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001817 if base_has_submodules:
1818 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819
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:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001824 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)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831 else:
1832 return 1
1833 viewvc_url = settings.GetViewVCUrl()
1834 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001835 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001836 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001837 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838 print ('Closing issue '
1839 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001840 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001841 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001842 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001843 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001844 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001845 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1846 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001847 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001848
1849 if retcode == 0:
1850 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1851 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001852 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854 return 0
1855
1856
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001857@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001858def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001859 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001861 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)
maruel@chromium.org90541732011-04-01 17:54:18 +00001869 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001870 return SendUpstream(parser, args, 'dcommit')
1871
1872
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001873@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001875 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876 if settings.GetIsGitSvn():
1877 print('This appears to be an SVN repository.')
1878 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001879 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880 return SendUpstream(parser, args, 'push')
1881
1882
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001883@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001885 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886 parser.add_option('-b', dest='newbranch',
1887 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001888 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001889 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001890 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',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001894 help='failed patches spew .rej files rather than '
1895 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896 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
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001902 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001904 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001905 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001906
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001907 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
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001914 return PatchIssue(issue_arg, options.reject, options.nocommit,
1915 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001916
1917
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001918def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001919 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001921 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001922 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001923 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001924 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001926 # Assume it's a URL to the patch. Default to https.
1927 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001928 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001929 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930 DieWithError('Must pass an issue ID or full URL for '
1931 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001932 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001933 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001934 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001935
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936 # 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942 # 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.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001946 try:
1947 patch_data = subprocess2.check_output(
1948 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1949 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950 DieWithError('Git patch mungling failed.')
1951 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001952 env = os.environ.copy()
1953 # 'cat' is a magical git string that disables pagers on all platforms.
1954 env['GIT_PAGER'] = 'cat'
1955
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001956 # 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).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001959 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001960 if directory:
1961 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001962 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001964 elif IsGitVersionAtLeast('1.7.12'):
1965 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001966 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001967 subprocess2.check_call(cmd, env=env,
1968 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001969 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970 DieWithError('Failed to apply the patch')
1971
1972 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001973 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1975 cl = Changelist()
1976 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001977 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001978 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 else:
1980 print "Patch applied to index."
1981 return 0
1982
1983
1984def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001985 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986 # 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.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001990 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)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995
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'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002008 return 'unset'
2009
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002010
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011def 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."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002014 url = settings.GetTreeStatusUrl()
2015 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016 connection = urllib2.urlopen(json_url)
2017 status = json.loads(connection.read())
2018 connection.close()
2019 return status['message']
2020
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002021
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002023 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002024 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002025 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
maruel@chromium.org15192402012-09-06 12:38:29 +00002038def 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.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002085 change = cl.GetChange(
2086 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2087 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002088 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 = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002100 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:
maruel@chromium.org15192402012-09-06 12:38:29 +00002104 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
stip@chromium.org43064fd2013-12-18 20:07:44 +00002112 for bot, tests in new_style:
2113 builders_and_tests.setdefault(bot, []).extend(tests)
2114
maruel@chromium.org15192402012-09-06 12:38:29 +00002115 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
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002121 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
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002129 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)
maruel@chromium.org15192402012-09-06 12:38:29 +00002135
2136 cl.RpcServer().trigger_try_jobs(
2137 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
2138 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002139 print('Tried jobs on:')
2140 length = max(len(builder) for builder in builders_and_tests)
2141 for builder in sorted(builders_and_tests):
2142 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002143 return 0
2144
2145
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002146@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002148 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002149 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002150 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002151 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002153 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002154 if args:
2155 # One arg means set upstream branch.
2156 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2157 cl = Changelist()
2158 print "Upstream branch set to " + cl.GetUpstreamBranch()
2159 else:
2160 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002161 return 0
2162
2163
thestig@chromium.org00858c82013-12-02 23:08:03 +00002164def CMDweb(parser, args):
2165 """Opens the current CL in the web browser."""
2166 _, args = parser.parse_args(args)
2167 if args:
2168 parser.error('Unrecognized args: %s' % ' '.join(args))
2169
2170 issue_url = Changelist().GetIssueURL()
2171 if not issue_url:
2172 print >> sys.stderr, 'ERROR No issue to open'
2173 return 1
2174
2175 webbrowser.open(issue_url)
2176 return 0
2177
2178
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002179def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002180 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002181 _, args = parser.parse_args(args)
2182 if args:
2183 parser.error('Unrecognized args: %s' % ' '.join(args))
2184 cl = Changelist()
2185 cl.SetFlag('commit', '1')
2186 return 0
2187
2188
groby@chromium.org411034a2013-02-26 15:12:01 +00002189def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002190 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002191 _, args = parser.parse_args(args)
2192 if args:
2193 parser.error('Unrecognized args: %s' % ' '.join(args))
2194 cl = Changelist()
2195 # Ensure there actually is an issue to close.
2196 cl.GetDescription()
2197 cl.CloseIssue()
2198 return 0
2199
2200
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002201def CMDdiff(parser, args):
2202 """shows differences between local tree and last upload."""
2203 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002204 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002205 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002206 if not issue:
2207 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002208 TMP_BRANCH = 'git-cl-diff'
2209 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2210
2211 # Create a new branch based on the merge-base
2212 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2213 try:
2214 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002215 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002216 if rtn != 0:
2217 return rtn
2218
2219 # Switch back to starting brand and diff against the temporary
2220 # branch containing the latest rietveld patch.
2221 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2222 finally:
2223 RunGit(['checkout', '-q', branch])
2224 RunGit(['branch', '-D', TMP_BRANCH])
2225
2226 return 0
2227
2228
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002229def CMDowners(parser, args):
2230 """interactively find the owners for reviewing"""
2231 parser.add_option(
2232 '--no-color',
2233 action='store_true',
2234 help='Use this option to disable color output')
2235 options, args = parser.parse_args(args)
2236
2237 author = RunGit(['config', 'user.email']).strip() or None
2238
2239 cl = Changelist()
2240
2241 if args:
2242 if len(args) > 1:
2243 parser.error('Unknown args')
2244 base_branch = args[0]
2245 else:
2246 # Default to diffing against the common ancestor of the upstream branch.
2247 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2248
2249 change = cl.GetChange(base_branch, None)
2250 return owners_finder.OwnersFinder(
2251 [f.LocalPath() for f in
2252 cl.GetChange(base_branch, None).AffectedFiles()],
2253 change.RepositoryRoot(), author,
2254 fopen=file, os_path=os.path, glob=glob.glob,
2255 disable_color=options.no_color).run()
2256
2257
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002258def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002259 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002260 CLANG_EXTS = ['.cc', '.cpp', '.h']
2261 parser.add_option('--full', action='store_true', default=False)
2262 opts, args = parser.parse_args(args)
2263 if args:
2264 parser.error('Unrecognized args: %s' % ' '.join(args))
2265
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002266 # git diff generates paths against the root of the repository. Change
2267 # to that directory so clang-format can find files even within subdirs.
2268 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2269 if rel_base_path:
2270 os.chdir(rel_base_path)
2271
digit@chromium.org29e47272013-05-17 17:01:46 +00002272 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002273 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002274 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002275 # Only list the names of modified files.
2276 diff_cmd.append('--name-only')
2277 else:
2278 # Only generate context-less patches.
2279 diff_cmd.append('-U0')
2280
2281 # Grab the merge-base commit, i.e. the upstream commit of the current
2282 # branch when it was created or the last time it was rebased. This is
2283 # to cover the case where the user may have called "git fetch origin",
2284 # moving the origin branch to a newer commit, but hasn't rebased yet.
2285 upstream_commit = None
2286 cl = Changelist()
2287 upstream_branch = cl.GetUpstreamBranch()
2288 if upstream_branch:
2289 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2290 upstream_commit = upstream_commit.strip()
2291
2292 if not upstream_commit:
2293 DieWithError('Could not find base commit for this branch. '
2294 'Are you in detached state?')
2295
2296 diff_cmd.append(upstream_commit)
2297
2298 # Handle source file filtering.
2299 diff_cmd.append('--')
2300 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2301 diff_output = RunGit(diff_cmd)
2302
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002303 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2304
digit@chromium.org29e47272013-05-17 17:01:46 +00002305 if opts.full:
2306 # diff_output is a list of files to send to clang-format.
2307 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002308 if not files:
2309 print "Nothing to format."
2310 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002311 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2312 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002313 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002314 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002315 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2316 'clang-format-diff.py')
2317 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002318 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002319 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002320
2321 # Newer versions of clang-format-diff.py require an explicit -i flag
2322 # to apply the edits to files, otherwise it just displays a diff.
2323 # Probe the usage string to verify if this is needed.
2324 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2325 if '[-i]' in help_text:
2326 cmd.append('-i')
2327
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002328 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002329
2330 return 0
2331
2332
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002333class OptionParser(optparse.OptionParser):
2334 """Creates the option parse and add --verbose support."""
2335 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002336 optparse.OptionParser.__init__(
2337 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002338 self.add_option(
2339 '-v', '--verbose', action='count', default=0,
2340 help='Use 2 times for more debugging info')
2341
2342 def parse_args(self, args=None, values=None):
2343 options, args = optparse.OptionParser.parse_args(self, args, values)
2344 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2345 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2346 return options, args
2347
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002348
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002349def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002350 if sys.hexversion < 0x02060000:
2351 print >> sys.stderr, (
2352 '\nYour python version %s is unsupported, please upgrade.\n' %
2353 sys.version.split(' ', 1)[0])
2354 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002355
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002356 # Reload settings.
2357 global settings
2358 settings = Settings()
2359
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002360 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002361 dispatcher = subcommand.CommandDispatcher(__name__)
2362 try:
2363 return dispatcher.execute(OptionParser(), argv)
2364 except urllib2.HTTPError, e:
2365 if e.code != 500:
2366 raise
2367 DieWithError(
2368 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2369 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002370
2371
2372if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002373 # These affect sys.stdout so do it outside of main() to simplify mocks in
2374 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002375 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002376 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002377 sys.exit(main(sys.argv[1:]))