blob: acd04a57af4bddebaefd66e69df71682fe7d04fc [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
27from third_party import upload
28import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000029import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000032import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000034import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import watchlists
36
37
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000038DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000039POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000041GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042
maruel@chromium.org90541732011-04-01 17:54:18 +000043
maruel@chromium.orgddd59412011-11-30 14:20:38 +000044# Initialized in main()
45settings = None
46
47
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000049 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050 sys.exit(1)
51
52
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000053def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000055 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000056 except subprocess2.CalledProcessError, e:
57 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059 'Command "%s" failed.\n%s' % (
60 ' '.join(args), error_message or e.stdout or ''))
61 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062
63
64def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065 """Returns stdout."""
66 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
68
69def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000071 try:
72 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
73 return code, out[0]
74 except ValueError:
75 # When the subprocess fails, it returns None. That triggers a ValueError
76 # when trying to unpack the return value into (out, code).
77 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
79
80def usage(more):
81 def hook(fn):
82 fn.usage_more = more
83 return fn
84 return hook
85
86
maruel@chromium.org90541732011-04-01 17:54:18 +000087def ask_for_data(prompt):
88 try:
89 return raw_input(prompt)
90 except KeyboardInterrupt:
91 # Hide the exception.
92 sys.exit(1)
93
94
iannucci@chromium.org53937ba2012-10-02 18:20:43 +000095def add_git_similarity(parser):
96 parser.add_option(
97 '--similarity', metavar='SIM', type='int', action='store', default=None,
98 help='Sets the percentage that a pair of files need to match in order to'
99 ' be considered copies (default 50)')
100
101 old_parser_args = parser.parse_args
102 def Parse(args):
103 options, args = old_parser_args(args)
104
105 branch = Changelist().GetBranch()
106 key = 'branch.%s.git-cl-similarity' % branch
107 if options.similarity is None:
108 if branch:
109 (_, stdout) = RunGitWithCode(['config', '--int', '--get', key])
110 try:
111 options.similarity = int(stdout.strip())
112 except ValueError:
113 pass
114 options.similarity = options.similarity or 50
115 else:
116 if branch:
117 print('Note: Saving similarity of %d%% in git config.'
118 % options.similarity)
119 RunGit(['config', '--int', key, str(options.similarity)])
120
121 options.similarity = max(1, min(options.similarity, 100))
122
123 print('Using %d%% similarity for rename/copy detection. '
124 'Override with --similarity.' % options.similarity)
125
126 return options, args
127 parser.parse_args = Parse
128
129
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000130def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
131 """Return the corresponding git ref if |base_url| together with |glob_spec|
132 matches the full |url|.
133
134 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
135 """
136 fetch_suburl, as_ref = glob_spec.split(':')
137 if allow_wildcards:
138 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
139 if glob_match:
140 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
141 # "branches/{472,597,648}/src:refs/remotes/svn/*".
142 branch_re = re.escape(base_url)
143 if glob_match.group(1):
144 branch_re += '/' + re.escape(glob_match.group(1))
145 wildcard = glob_match.group(2)
146 if wildcard == '*':
147 branch_re += '([^/]*)'
148 else:
149 # Escape and replace surrounding braces with parentheses and commas
150 # with pipe symbols.
151 wildcard = re.escape(wildcard)
152 wildcard = re.sub('^\\\\{', '(', wildcard)
153 wildcard = re.sub('\\\\,', '|', wildcard)
154 wildcard = re.sub('\\\\}$', ')', wildcard)
155 branch_re += wildcard
156 if glob_match.group(3):
157 branch_re += re.escape(glob_match.group(3))
158 match = re.match(branch_re, url)
159 if match:
160 return re.sub('\*$', match.group(1), as_ref)
161
162 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
163 if fetch_suburl:
164 full_url = base_url + '/' + fetch_suburl
165 else:
166 full_url = base_url
167 if full_url == url:
168 return as_ref
169 return None
170
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000171
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000172def print_stats(similarity, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000173 """Prints statistics about the change to the user."""
174 # --no-ext-diff is broken in some versions of Git, so try to work around
175 # this by overriding the environment (but there is still a problem if the
176 # git config key "diff.external" is used).
177 env = os.environ.copy()
178 if 'GIT_EXTERNAL_DIFF' in env:
179 del env['GIT_EXTERNAL_DIFF']
180 return subprocess2.call(
iannucci@chromium.org1512ab62012-09-11 01:30:56 +0000181 ['git', 'diff', '--no-ext-diff', '--stat', '--find-copies-harder',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182 '-C%s' % similarity, '-l100000'] + args, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000183
184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000185class Settings(object):
186 def __init__(self):
187 self.default_server = None
188 self.cc = None
189 self.root = None
190 self.is_git_svn = None
191 self.svn_branch = None
192 self.tree_status_url = None
193 self.viewvc_url = None
194 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000195 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000196
197 def LazyUpdateIfNeeded(self):
198 """Updates the settings from a codereview.settings file, if available."""
199 if not self.updated:
200 cr_settings_file = FindCodereviewSettingsFile()
201 if cr_settings_file:
202 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000203 self.updated = True
204 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000205 self.updated = True
206
207 def GetDefaultServerUrl(self, error_ok=False):
208 if not self.default_server:
209 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000210 self.default_server = gclient_utils.UpgradeToHttps(
211 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000212 if error_ok:
213 return self.default_server
214 if not self.default_server:
215 error_message = ('Could not find settings file. You must configure '
216 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000217 self.default_server = gclient_utils.UpgradeToHttps(
218 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000219 return self.default_server
220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000221 def GetRoot(self):
222 if not self.root:
223 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
224 return self.root
225
226 def GetIsGitSvn(self):
227 """Return true if this repo looks like it's using git-svn."""
228 if self.is_git_svn is None:
229 # If you have any "svn-remote.*" config keys, we think you're using svn.
230 self.is_git_svn = RunGitWithCode(
231 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
232 return self.is_git_svn
233
234 def GetSVNBranch(self):
235 if self.svn_branch is None:
236 if not self.GetIsGitSvn():
237 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
238
239 # Try to figure out which remote branch we're based on.
240 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000241 # 1) iterate through our branch history and find the svn URL.
242 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000243
244 # regexp matching the git-svn line that contains the URL.
245 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
246
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000247 # We don't want to go through all of history, so read a line from the
248 # pipe at a time.
249 # The -100 is an arbitrary limit so we don't search forever.
250 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000251 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000252 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000253 for line in proc.stdout:
254 match = git_svn_re.match(line)
255 if match:
256 url = match.group(1)
257 proc.stdout.close() # Cut pipe.
258 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000259
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000260 if url:
261 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
262 remotes = RunGit(['config', '--get-regexp',
263 r'^svn-remote\..*\.url']).splitlines()
264 for remote in remotes:
265 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000267 remote = match.group(1)
268 base_url = match.group(2)
269 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000270 ['config', 'svn-remote.%s.fetch' % remote],
271 error_ok=True).strip()
272 if fetch_spec:
273 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
274 if self.svn_branch:
275 break
276 branch_spec = RunGit(
277 ['config', 'svn-remote.%s.branches' % remote],
278 error_ok=True).strip()
279 if branch_spec:
280 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
281 if self.svn_branch:
282 break
283 tag_spec = RunGit(
284 ['config', 'svn-remote.%s.tags' % remote],
285 error_ok=True).strip()
286 if tag_spec:
287 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
288 if self.svn_branch:
289 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290
291 if not self.svn_branch:
292 DieWithError('Can\'t guess svn branch -- try specifying it on the '
293 'command line')
294
295 return self.svn_branch
296
297 def GetTreeStatusUrl(self, error_ok=False):
298 if not self.tree_status_url:
299 error_message = ('You must configure your tree status URL by running '
300 '"git cl config".')
301 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
302 error_ok=error_ok,
303 error_message=error_message)
304 return self.tree_status_url
305
306 def GetViewVCUrl(self):
307 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000308 self.viewvc_url = gclient_utils.UpgradeToHttps(
309 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 return self.viewvc_url
311
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000312 def GetDefaultCCList(self):
313 return self._GetConfig('rietveld.cc', error_ok=True)
314
ukai@chromium.orge8077812012-02-03 03:41:46 +0000315 def GetIsGerrit(self):
316 """Return true if this repo is assosiated with gerrit code review system."""
317 if self.is_gerrit is None:
318 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
319 return self.is_gerrit
320
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321 def _GetConfig(self, param, **kwargs):
322 self.LazyUpdateIfNeeded()
323 return RunGit(['config', param], **kwargs).strip()
324
325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326def ShortBranchName(branch):
327 """Convert a name like 'refs/heads/foo' to just 'foo'."""
328 return branch.replace('refs/heads/', '')
329
330
331class Changelist(object):
332 def __init__(self, branchref=None):
333 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000334 global settings
335 if not settings:
336 # Happens when git_cl.py is used as a utility library.
337 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000338 settings.GetDefaultServerUrl()
339 self.branchref = branchref
340 if self.branchref:
341 self.branch = ShortBranchName(self.branchref)
342 else:
343 self.branch = None
344 self.rietveld_server = None
345 self.upstream_branch = None
346 self.has_issue = False
347 self.issue = None
348 self.has_description = False
349 self.description = None
350 self.has_patchset = False
351 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000352 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000353 self.cc = None
354 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000355 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000356
357 def GetCCList(self):
358 """Return the users cc'd on this CL.
359
360 Return is a string suitable for passing to gcl with the --cc flag.
361 """
362 if self.cc is None:
363 base_cc = settings .GetDefaultCCList()
364 more_cc = ','.join(self.watchers)
365 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
366 return self.cc
367
368 def SetWatchers(self, watchers):
369 """Set the list of email addresses that should be cc'd based on the changed
370 files in this CL.
371 """
372 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000373
374 def GetBranch(self):
375 """Returns the short branch name, e.g. 'master'."""
376 if not self.branch:
377 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
378 self.branch = ShortBranchName(self.branchref)
379 return self.branch
380
381 def GetBranchRef(self):
382 """Returns the full branch name, e.g. 'refs/heads/master'."""
383 self.GetBranch() # Poke the lazy loader.
384 return self.branchref
385
386 def FetchUpstreamTuple(self):
387 """Returns a tuple containg remote and remote ref,
388 e.g. 'origin', 'refs/heads/master'
389 """
390 remote = '.'
391 branch = self.GetBranch()
392 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
393 error_ok=True).strip()
394 if upstream_branch:
395 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
396 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000397 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
398 error_ok=True).strip()
399 if upstream_branch:
400 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000402 # Fall back on trying a git-svn upstream branch.
403 if settings.GetIsGitSvn():
404 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000405 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000406 # Else, try to guess the origin remote.
407 remote_branches = RunGit(['branch', '-r']).split()
408 if 'origin/master' in remote_branches:
409 # Fall back on origin/master if it exits.
410 remote = 'origin'
411 upstream_branch = 'refs/heads/master'
412 elif 'origin/trunk' in remote_branches:
413 # Fall back on origin/trunk if it exists. Generally a shared
414 # git-svn clone
415 remote = 'origin'
416 upstream_branch = 'refs/heads/trunk'
417 else:
418 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419Either pass complete "git diff"-style arguments, like
420 git cl upload origin/master
421or verify this branch is set up to track another (via the --track argument to
422"git checkout -b ...").""")
423
424 return remote, upstream_branch
425
426 def GetUpstreamBranch(self):
427 if self.upstream_branch is None:
428 remote, upstream_branch = self.FetchUpstreamTuple()
429 if remote is not '.':
430 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
431 self.upstream_branch = upstream_branch
432 return self.upstream_branch
433
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000434 def GetRemote(self):
435 if not self._remote:
436 self._remote = self.FetchUpstreamTuple()[0]
437 if self._remote == '.':
438
439 remotes = RunGit(['remote'], error_ok=True).split()
440 if len(remotes) == 1:
441 self._remote, = remotes
442 elif 'origin' in remotes:
443 self._remote = 'origin'
444 logging.warning('Could not determine which remote this change is '
445 'associated with, so defaulting to "%s". This may '
446 'not be what you want. You may prevent this message '
447 'by running "git svn info" as documented here: %s',
448 self._remote,
449 GIT_INSTRUCTIONS_URL)
450 else:
451 logging.warn('Could not determine which remote this change is '
452 'associated with. You may prevent this message by '
453 'running "git svn info" as documented here: %s',
454 GIT_INSTRUCTIONS_URL)
455 return self._remote
456
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000457 def GetGitBaseUrlFromConfig(self):
458 """Return the configured base URL from branch.<branchname>.baseurl.
459
460 Returns None if it is not set.
461 """
462 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
463 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000464
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 def GetRemoteUrl(self):
466 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
467
468 Returns None if there is no remote.
469 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000470 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
472
473 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000474 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000475 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
477 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000478 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000479 else:
480 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 self.has_issue = True
482 return self.issue
483
484 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000485 if not self.rietveld_server:
486 # If we're on a branch then get the server potentially associated
487 # with that branch.
488 if self.GetIssue():
489 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
490 ['config', self._RietveldServer()], error_ok=True).strip())
491 if not self.rietveld_server:
492 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 return self.rietveld_server
494
495 def GetIssueURL(self):
496 """Get the URL for a particular issue."""
497 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
498
499 def GetDescription(self, pretty=False):
500 if not self.has_description:
501 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000502 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000503 try:
504 self.description = self.RpcServer().get_description(issue).strip()
505 except urllib2.HTTPError, e:
506 if e.code == 404:
507 DieWithError(
508 ('\nWhile fetching the description for issue %d, received a '
509 '404 (not found)\n'
510 'error. It is likely that you deleted this '
511 'issue on the server. If this is the\n'
512 'case, please run\n\n'
513 ' git cl issue 0\n\n'
514 'to clear the association with the deleted issue. Then run '
515 'this command again.') % issue)
516 else:
517 DieWithError(
518 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000519 self.has_description = True
520 if pretty:
521 wrapper = textwrap.TextWrapper()
522 wrapper.initial_indent = wrapper.subsequent_indent = ' '
523 return wrapper.fill(self.description)
524 return self.description
525
526 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000527 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000528 if not self.has_patchset:
529 patchset = RunGit(['config', self._PatchsetSetting()],
530 error_ok=True).strip()
531 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000532 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000533 else:
534 self.patchset = None
535 self.has_patchset = True
536 return self.patchset
537
538 def SetPatchset(self, patchset):
539 """Set this branch's patchset. If patchset=0, clears the patchset."""
540 if patchset:
541 RunGit(['config', self._PatchsetSetting(), str(patchset)])
542 else:
543 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000544 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000545 self.has_patchset = False
546
binji@chromium.org0281f522012-09-14 13:37:59 +0000547 def GetMostRecentPatchset(self, issue):
548 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000549 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000550
551 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000552 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000553 '/download/issue%s_%s.diff' % (issue, patchset))
554
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000555 def SetIssue(self, issue):
556 """Set this branch's issue. If issue=0, clears the issue."""
557 if issue:
558 RunGit(['config', self._IssueSetting(), str(issue)])
559 if self.rietveld_server:
560 RunGit(['config', self._RietveldServer(), self.rietveld_server])
561 else:
562 RunGit(['config', '--unset', self._IssueSetting()])
563 self.SetPatchset(0)
564 self.has_issue = False
565
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000566 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000567 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
568 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000569
570 # We use the sha1 of HEAD as a name of this change.
571 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000572 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000573 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000574 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000575 except subprocess2.CalledProcessError:
576 DieWithError(
577 ('\nFailed to diff against upstream branch %s!\n\n'
578 'This branch probably doesn\'t exist anymore. To reset the\n'
579 'tracking branch, please run\n'
580 ' git branch --set-upstream %s trunk\n'
581 'replacing trunk with origin/master or the relevant branch') %
582 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000583
maruel@chromium.org52424302012-08-29 15:14:30 +0000584 issue = self.GetIssue()
585 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000586 if issue:
587 description = self.GetDescription()
588 else:
589 # If the change was never uploaded, use the log messages of all commits
590 # up to the branch point, as git cl upload will prefill the description
591 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000592 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
593 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000594
595 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000596 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000597 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000598 name,
599 description,
600 absroot,
601 files,
602 issue,
603 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000604 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000605
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000606 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
607 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
608 change = self.GetChange(upstream_branch, author)
609
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000610 # Apply watchlists on upload.
611 if not committing:
612 watchlist = watchlists.Watchlists(change.RepositoryRoot())
613 files = [f.LocalPath() for f in change.AffectedFiles()]
614 self.SetWatchers(watchlist.GetWatchersForPaths(files))
615
616 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000617 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000618 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000619 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000620 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000621 except presubmit_support.PresubmitFailure, e:
622 DieWithError(
623 ('%s\nMaybe your depot_tools is out of date?\n'
624 'If all fails, contact maruel@') % e)
625
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000627 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000628 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000629 self.RpcServer().update_description(issue, self.description)
630 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000632 def SetFlag(self, flag, value):
633 """Patchset must match."""
634 if not self.GetPatchset():
635 DieWithError('The patchset needs to match. Send another patchset.')
636 try:
637 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000638 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000639 except urllib2.HTTPError, e:
640 if e.code == 404:
641 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
642 if e.code == 403:
643 DieWithError(
644 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
645 'match?') % (self.GetIssue(), self.GetPatchset()))
646 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000648 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 """Returns an upload.RpcServer() to access this review's rietveld instance.
650 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000651 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000652 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
653 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000654 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655
656 def _IssueSetting(self):
657 """Return the git setting that stores this change's issue."""
658 return 'branch.%s.rietveldissue' % self.GetBranch()
659
660 def _PatchsetSetting(self):
661 """Return the git setting that stores this change's most recent patchset."""
662 return 'branch.%s.rietveldpatchset' % self.GetBranch()
663
664 def _RietveldServer(self):
665 """Returns the git setting that stores this change's rietveld server."""
666 return 'branch.%s.rietveldserver' % self.GetBranch()
667
668
669def GetCodereviewSettingsInteractively():
670 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000671 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672 server = settings.GetDefaultServerUrl(error_ok=True)
673 prompt = 'Rietveld server (host[:port])'
674 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000675 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 if not server and not newserver:
677 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000678 if newserver:
679 newserver = gclient_utils.UpgradeToHttps(newserver)
680 if newserver != server:
681 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000682
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000683 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684 prompt = caption
685 if initial:
686 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000687 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000688 if new_val == 'x':
689 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000690 elif new_val:
691 if is_url:
692 new_val = gclient_utils.UpgradeToHttps(new_val)
693 if new_val != initial:
694 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000696 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000698 'tree-status-url', False)
699 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700
701 # TODO: configure a default branch to diff against, rather than this
702 # svn-based hackery.
703
704
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000705class ChangeDescription(object):
706 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000707 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000708 self.log_desc = log_desc
709 self.reviewers = reviewers
710 self.description = self.log_desc
711
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000712 def Prompt(self):
713 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000714# This will displayed on the codereview site.
715# The first line will also be used as the subject of the review.
716"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000717 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000718 if ('\nR=' not in self.description and
719 '\nTBR=' not in self.description and
720 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000721 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000722 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000723 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000724 content = content.rstrip('\n') + '\n'
725 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000726 if not content:
727 DieWithError('Running editor failed')
728 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000729 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000730 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000731 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000732
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000733 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000734 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000735 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000736 # Retrieves all reviewer lines
737 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000738 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000739 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000740 if reviewers:
741 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000742
743 def IsEmpty(self):
744 return not self.description
745
746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747def FindCodereviewSettingsFile(filename='codereview.settings'):
748 """Finds the given file starting in the cwd and going up.
749
750 Only looks up to the top of the repository unless an
751 'inherit-review-settings-ok' file exists in the root of the repository.
752 """
753 inherit_ok_file = 'inherit-review-settings-ok'
754 cwd = os.getcwd()
755 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
756 if os.path.isfile(os.path.join(root, inherit_ok_file)):
757 root = '/'
758 while True:
759 if filename in os.listdir(cwd):
760 if os.path.isfile(os.path.join(cwd, filename)):
761 return open(os.path.join(cwd, filename))
762 if cwd == root:
763 break
764 cwd = os.path.dirname(cwd)
765
766
767def LoadCodereviewSettingsFromFile(fileobj):
768 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000769 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771 def SetProperty(name, setting, unset_error_ok=False):
772 fullname = 'rietveld.' + name
773 if setting in keyvals:
774 RunGit(['config', fullname, keyvals[setting]])
775 else:
776 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
777
778 SetProperty('server', 'CODE_REVIEW_SERVER')
779 # Only server setting is required. Other settings can be absent.
780 # In that case, we ignore errors raised during option deletion attempt.
781 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
782 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
783 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
784
ukai@chromium.orge8077812012-02-03 03:41:46 +0000785 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
786 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
787 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000788
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
790 #should be of the form
791 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
792 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
793 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
794 keyvals['ORIGIN_URL_CONFIG']])
795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000797def urlretrieve(source, destination):
798 """urllib is broken for SSL connections via a proxy therefore we
799 can't use urllib.urlretrieve()."""
800 with open(destination, 'w') as f:
801 f.write(urllib2.urlopen(source).read())
802
803
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000804def DownloadHooks(force):
805 """downloads hooks
806
807 Args:
808 force: True to update hooks. False to install hooks if not present.
809 """
810 if not settings.GetIsGerrit():
811 return
812 server_url = settings.GetDefaultServerUrl()
813 src = '%s/tools/hooks/commit-msg' % server_url
814 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
815 if not os.access(dst, os.X_OK):
816 if os.path.exists(dst):
817 if not force:
818 return
819 os.remove(dst)
820 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000821 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000822 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
823 except Exception:
824 if os.path.exists(dst):
825 os.remove(dst)
826 DieWithError('\nFailed to download hooks from %s' % src)
827
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829@usage('[repo root containing codereview.settings]')
830def CMDconfig(parser, args):
831 """edit configuration for this tree"""
832
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000833 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 if len(args) == 0:
835 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000836 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 return 0
838
839 url = args[0]
840 if not url.endswith('codereview.settings'):
841 url = os.path.join(url, 'codereview.settings')
842
843 # Load code review settings and download hooks (if available).
844 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000845 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 return 0
847
848
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000849def CMDbaseurl(parser, args):
850 """get or set base-url for this branch"""
851 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
852 branch = ShortBranchName(branchref)
853 _, args = parser.parse_args(args)
854 if not args:
855 print("Current base-url:")
856 return RunGit(['config', 'branch.%s.base-url' % branch],
857 error_ok=False).strip()
858 else:
859 print("Setting base-url to %s" % args[0])
860 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
861 error_ok=False).strip()
862
863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864def CMDstatus(parser, args):
865 """show status of changelists"""
866 parser.add_option('--field',
867 help='print only specific field (desc|id|patch|url)')
868 (options, args) = parser.parse_args(args)
869
870 # TODO: maybe make show_branches a flag if necessary.
871 show_branches = not options.field
872
873 if show_branches:
874 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
875 if branches:
876 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000877 changes = (Changelist(branchref=b) for b in branches.splitlines())
878 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
879 alignment = max(5, max(len(b) for b in branches))
880 for branch in sorted(branches):
881 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882
883 cl = Changelist()
884 if options.field:
885 if options.field.startswith('desc'):
886 print cl.GetDescription()
887 elif options.field == 'id':
888 issueid = cl.GetIssue()
889 if issueid:
890 print issueid
891 elif options.field == 'patch':
892 patchset = cl.GetPatchset()
893 if patchset:
894 print patchset
895 elif options.field == 'url':
896 url = cl.GetIssueURL()
897 if url:
898 print url
899 else:
900 print
901 print 'Current branch:',
902 if not cl.GetIssue():
903 print 'no issue assigned.'
904 return 0
905 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000906 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 print 'Issue description:'
908 print cl.GetDescription(pretty=True)
909 return 0
910
911
912@usage('[issue_number]')
913def CMDissue(parser, args):
914 """Set or display the current code review issue number.
915
916 Pass issue number 0 to clear the current issue.
917"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000918 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919
920 cl = Changelist()
921 if len(args) > 0:
922 try:
923 issue = int(args[0])
924 except ValueError:
925 DieWithError('Pass a number to set the issue or none to list it.\n'
926 'Maybe you want to run git cl status?')
927 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +0000928 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 return 0
930
931
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000932def CMDcomments(parser, args):
933 """show review comments of the current changelist"""
934 (_, args) = parser.parse_args(args)
935 if args:
936 parser.error('Unsupported argument: %s' % args)
937
938 cl = Changelist()
939 if cl.GetIssue():
940 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
941 for message in sorted(data['messages'], key=lambda x: x['date']):
942 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
943 if message['text'].strip():
944 print '\n'.join(' ' + l for l in message['text'].splitlines())
945 return 0
946
947
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948def CreateDescriptionFromLog(args):
949 """Pulls out the commit log to use as a base for the CL description."""
950 log_args = []
951 if len(args) == 1 and not args[0].endswith('.'):
952 log_args = [args[0] + '..']
953 elif len(args) == 1 and args[0].endswith('...'):
954 log_args = [args[0][:-1]]
955 elif len(args) == 2:
956 log_args = [args[0] + '..' + args[1]]
957 else:
958 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000959 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960
961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962def CMDpresubmit(parser, args):
963 """run presubmit tests on the current changelist"""
964 parser.add_option('--upload', action='store_true',
965 help='Run upload hook instead of the push/dcommit hook')
sbc@chromium.org495ad152012-09-04 23:07:42 +0000966 parser.add_option('--force', action='store_true',
967 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 (options, args) = parser.parse_args(args)
969
970 # Make sure index is up-to-date before running diff-index.
971 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
sbc@chromium.org495ad152012-09-04 23:07:42 +0000972 if not options.force and RunGit(['diff-index', 'HEAD']):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973 # TODO(maruel): Is this really necessary?
sbc@chromium.org495ad152012-09-04 23:07:42 +0000974 print ('Cannot presubmit with a dirty tree.\n'
975 'You must commit locally first (or use --force).')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976 return 1
977
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000978 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979 if args:
980 base_branch = args[0]
981 else:
982 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000983 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000985 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000986 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000987 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000988 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989
990
ukai@chromium.orge8077812012-02-03 03:41:46 +0000991def GerritUpload(options, args, cl):
992 """upload the current branch to gerrit."""
993 # We assume the remote called "origin" is the one we want.
994 # It is probably not worthwhile to support different workflows.
995 remote = 'origin'
996 branch = 'master'
997 if options.target_branch:
998 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001000 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001001 if options.reviewers:
1002 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001003 change_desc = ChangeDescription(log_desc, options.reviewers)
1004 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +00001005 if change_desc.IsEmpty():
1006 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007 return 1
1008
ukai@chromium.orge8077812012-02-03 03:41:46 +00001009 receive_options = []
1010 cc = cl.GetCCList().split(',')
1011 if options.cc:
1012 cc += options.cc.split(',')
1013 cc = filter(None, cc)
1014 if cc:
1015 receive_options += ['--cc=' + email for email in cc]
1016 if change_desc.reviewers:
1017 reviewers = filter(None, change_desc.reviewers.split(','))
1018 if reviewers:
1019 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020
ukai@chromium.orge8077812012-02-03 03:41:46 +00001021 git_command = ['push']
1022 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001023 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001024 ' '.join(receive_options))
1025 git_command += [remote, 'HEAD:refs/for/' + branch]
1026 RunGit(git_command)
1027 # TODO(ukai): parse Change-Id: and set issue number?
1028 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001029
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030
ukai@chromium.orge8077812012-02-03 03:41:46 +00001031def RietveldUpload(options, args, cl):
1032 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1034 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001035 if options.emulate_svn_auto_props:
1036 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
1038 change_desc = None
1039
1040 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001041 if options.title:
1042 upload_args.extend(['--title', options.title])
1043 elif options.message:
1044 # TODO(rogerta): for now, the -m option will also set the --title option
1045 # for upload.py. Soon this will be changed to set the --message option.
1046 # Will wait until people are used to typing -t instead of -m.
1047 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001048 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 print ("This branch is associated with issue %s. "
1050 "Adding patch to that issue." % cl.GetIssue())
1051 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001052 if options.title:
1053 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001054 message = options.message or CreateDescriptionFromLog(args)
1055 change_desc = ChangeDescription(message, options.reviewers)
1056 if not options.force:
1057 change_desc.Prompt()
1058 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001059
1060 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 print "Description is empty; aborting."
1062 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001063
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001064 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001065 if change_desc.reviewers:
1066 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001067 if options.send_mail:
1068 if not change_desc.reviewers:
1069 DieWithError("Must specify reviewers to send email.")
1070 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001071 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001072 if cc:
1073 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001075 upload_args.extend(['--git_similarity', str(options.similarity)])
1076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 # Include the upstream repo's URL in the change -- this is useful for
1078 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001079 remote_url = cl.GetGitBaseUrlFromConfig()
1080 if not remote_url:
1081 if settings.GetIsGitSvn():
1082 # URL is dependent on the current directory.
1083 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1084 if data:
1085 keys = dict(line.split(': ', 1) for line in data.splitlines()
1086 if ': ' in line)
1087 remote_url = keys.get('URL', None)
1088 else:
1089 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1090 remote_url = (cl.GetRemoteUrl() + '@'
1091 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 if remote_url:
1093 upload_args.extend(['--base_url', remote_url])
1094
1095 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001096 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001097 except KeyboardInterrupt:
1098 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 except:
1100 # If we got an exception after the user typed a description for their
1101 # change, back up the description before re-raising.
1102 if change_desc:
1103 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1104 print '\nGot exception while uploading -- saving description to %s\n' \
1105 % backup_path
1106 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001107 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 backup_file.close()
1109 raise
1110
1111 if not cl.GetIssue():
1112 cl.SetIssue(issue)
1113 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001114
1115 if options.use_commit_queue:
1116 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 return 0
1118
1119
ukai@chromium.orge8077812012-02-03 03:41:46 +00001120@usage('[args to "git diff"]')
1121def CMDupload(parser, args):
1122 """upload the current changelist to codereview"""
1123 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1124 help='bypass upload presubmit hook')
1125 parser.add_option('-f', action='store_true', dest='force',
1126 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001127 parser.add_option('-m', dest='message', help='message for patchset')
1128 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001129 parser.add_option('-r', '--reviewers',
1130 help='reviewer email addresses')
1131 parser.add_option('--cc',
1132 help='cc email addresses')
1133 parser.add_option('--send-mail', action='store_true',
1134 help='send email to reviewer immediately')
1135 parser.add_option("--emulate_svn_auto_props", action="store_true",
1136 dest="emulate_svn_auto_props",
1137 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001138 parser.add_option('-c', '--use-commit-queue', action='store_true',
1139 help='tell the commit queue to commit this patchset')
1140 if settings.GetIsGerrit():
1141 parser.add_option('--target_branch', dest='target_branch', default='master',
1142 help='target branch to upload')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001143 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001144 (options, args) = parser.parse_args(args)
1145
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001146 # Print warning if the user used the -m/--message argument. This will soon
1147 # change to -t/--title.
1148 if options.message:
1149 print >> sys.stderr, (
1150 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1151 'In the near future, -m or --message will send a message instead.\n'
1152 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001153
ukai@chromium.orge8077812012-02-03 03:41:46 +00001154 # Make sure index is up-to-date before running diff-index.
1155 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1156 if RunGit(['diff-index', 'HEAD']):
1157 print 'Cannot upload with a dirty tree. You must commit locally first.'
1158 return 1
1159
1160 cl = Changelist()
1161 if args:
1162 # TODO(ukai): is it ok for gerrit case?
1163 base_branch = args[0]
1164 else:
1165 # Default to diffing against the "upstream" branch.
1166 base_branch = cl.GetUpstreamBranch()
1167 args = [base_branch + "..."]
1168
1169 if not options.bypass_hooks:
1170 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1171 may_prompt=not options.force,
1172 verbose=options.verbose,
1173 author=None)
1174 if not hook_results.should_continue():
1175 return 1
1176 if not options.reviewers and hook_results.reviewers:
1177 options.reviewers = hook_results.reviewers
1178
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001179 print_stats(options.similarity, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001180 if settings.GetIsGerrit():
1181 return GerritUpload(options, args, cl)
1182 return RietveldUpload(options, args, cl)
1183
1184
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001185def IsSubmoduleMergeCommit(ref):
1186 # When submodules are added to the repo, we expect there to be a single
1187 # non-git-svn merge commit at remote HEAD with a signature comment.
1188 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001189 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001190 return RunGit(cmd) != ''
1191
1192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193def SendUpstream(parser, args, cmd):
1194 """Common code for CmdPush and CmdDCommit
1195
1196 Squashed commit into a single.
1197 Updates changelog with metadata (e.g. pointer to review).
1198 Pushes/dcommits the code upstream.
1199 Updates review and closes.
1200 """
1201 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1202 help='bypass upload presubmit hook')
1203 parser.add_option('-m', dest='message',
1204 help="override review description")
1205 parser.add_option('-f', action='store_true', dest='force',
1206 help="force yes to questions (don't prompt)")
1207 parser.add_option('-c', dest='contributor',
1208 help="external contributor for patch (appended to " +
1209 "description and used as author for git). Should be " +
1210 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001211 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 (options, args) = parser.parse_args(args)
1213 cl = Changelist()
1214
1215 if not args or cmd == 'push':
1216 # Default to merging against our best guess of the upstream branch.
1217 args = [cl.GetUpstreamBranch()]
1218
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001219 if options.contributor:
1220 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1221 print "Please provide contibutor as 'First Last <email@example.com>'"
1222 return 1
1223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001225 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001227 # Make sure index is up-to-date before running diff-index.
1228 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 if RunGit(['diff-index', 'HEAD']):
1230 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1231 return 1
1232
1233 # This rev-list syntax means "show all commits not in my branch that
1234 # are in base_branch".
1235 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1236 base_branch]).splitlines()
1237 if upstream_commits:
1238 print ('Base branch "%s" has %d commits '
1239 'not in this branch.' % (base_branch, len(upstream_commits)))
1240 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1241 return 1
1242
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001243 # This is the revision `svn dcommit` will commit on top of.
1244 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1245 '--pretty=format:%H'])
1246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001248 # If the base_head is a submodule merge commit, the first parent of the
1249 # base_head should be a git-svn commit, which is what we're interested in.
1250 base_svn_head = base_branch
1251 if base_has_submodules:
1252 base_svn_head += '^1'
1253
1254 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 if extra_commits:
1256 print ('This branch has %d additional commits not upstreamed yet.'
1257 % len(extra_commits.splitlines()))
1258 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1259 'before attempting to %s.' % (base_branch, cmd))
1260 return 1
1261
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001262 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001263 author = None
1264 if options.contributor:
1265 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001266 hook_results = cl.RunHook(
1267 committing=True,
1268 upstream_branch=base_branch,
1269 may_prompt=not options.force,
1270 verbose=options.verbose,
1271 author=author)
1272 if not hook_results.should_continue():
1273 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274
1275 if cmd == 'dcommit':
1276 # Check the tree status if the tree status URL is set.
1277 status = GetTreeStatus()
1278 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001279 print('The tree is closed. Please wait for it to reopen. Use '
1280 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281 return 1
1282 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001283 print('Unable to determine tree status. Please verify manually and '
1284 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001285 else:
1286 breakpad.SendStack(
1287 'GitClHooksBypassedCommit',
1288 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001289 (cl.GetRietveldServer(), cl.GetIssue()),
1290 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291
1292 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001293 if not description and cl.GetIssue():
1294 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001296 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001297 if not cl.GetIssue() and options.bypass_hooks:
1298 description = CreateDescriptionFromLog([base_branch])
1299 else:
1300 print 'No description set.'
1301 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1302 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001304 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306
1307 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 description += "\nPatch from %s." % options.contributor
1309 print 'Description:', repr(description)
1310
1311 branches = [base_branch, cl.GetBranchRef()]
1312 if not options.force:
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001313 print_stats(options.similarity, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001314 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001316 # We want to squash all this branch's commits into one commit with the proper
1317 # description. We do this by doing a "reset --soft" to the base branch (which
1318 # keeps the working copy the same), then dcommitting that. If origin/master
1319 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1320 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001322 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1323 # Delete the branches if they exist.
1324 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1325 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1326 result = RunGitWithCode(showref_cmd)
1327 if result[0] == 0:
1328 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329
1330 # We might be in a directory that's present in this branch but not in the
1331 # trunk. Move up to the top of the tree so that git commands that expect a
1332 # valid CWD won't fail after we check out the merge branch.
1333 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1334 if rel_base_path:
1335 os.chdir(rel_base_path)
1336
1337 # Stuff our change into the merge branch.
1338 # We wrap in a try...finally block so if anything goes wrong,
1339 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001340 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001342 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1343 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 if options.contributor:
1345 RunGit(['commit', '--author', options.contributor, '-m', description])
1346 else:
1347 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001348 if base_has_submodules:
1349 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1350 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1351 RunGit(['checkout', CHERRY_PICK_BRANCH])
1352 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 if cmd == 'push':
1354 # push the merge branch.
1355 remote, branch = cl.FetchUpstreamTuple()
1356 retcode, output = RunGitWithCode(
1357 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1358 logging.debug(output)
1359 else:
1360 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001361 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001362 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001363 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364 finally:
1365 # And then swap back to the original branch and clean up.
1366 RunGit(['checkout', '-q', cl.GetBranch()])
1367 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001368 if base_has_submodules:
1369 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
1371 if cl.GetIssue():
1372 if cmd == 'dcommit' and 'Committed r' in output:
1373 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1374 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001375 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1376 for l in output.splitlines(False))
1377 match = filter(None, match)
1378 if len(match) != 1:
1379 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1380 output)
1381 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 else:
1383 return 1
1384 viewvc_url = settings.GetViewVCUrl()
1385 if viewvc_url and revision:
1386 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1387 print ('Closing issue '
1388 '(you may be prompted for your codereview password)...')
1389 cl.CloseIssue()
1390 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001391
1392 if retcode == 0:
1393 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1394 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001395 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 return 0
1398
1399
1400@usage('[upstream branch to apply against]')
1401def CMDdcommit(parser, args):
1402 """commit the current changelist via git-svn"""
1403 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001404 message = """This doesn't appear to be an SVN repository.
1405If your project has a git mirror with an upstream SVN master, you probably need
1406to run 'git svn init', see your project's git mirror documentation.
1407If your project has a true writeable upstream repository, you probably want
1408to run 'git cl push' instead.
1409Choose wisely, if you get this wrong, your commit might appear to succeed but
1410will instead be silently ignored."""
1411 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001412 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 return SendUpstream(parser, args, 'dcommit')
1414
1415
1416@usage('[upstream branch to apply against]')
1417def CMDpush(parser, args):
1418 """commit the current changelist via git"""
1419 if settings.GetIsGitSvn():
1420 print('This appears to be an SVN repository.')
1421 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001422 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 return SendUpstream(parser, args, 'push')
1424
1425
1426@usage('<patch url or issue id>')
1427def CMDpatch(parser, args):
1428 """patch in a code review"""
1429 parser.add_option('-b', dest='newbranch',
1430 help='create a new branch off trunk for the patch')
1431 parser.add_option('-f', action='store_true', dest='force',
1432 help='with -b, clobber any existing branch')
1433 parser.add_option('--reject', action='store_true', dest='reject',
1434 help='allow failed patches and spew .rej files')
1435 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1436 help="don't commit after patch applies")
1437 (options, args) = parser.parse_args(args)
1438 if len(args) != 1:
1439 parser.print_help()
1440 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001441 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001443 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001444 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001445
maruel@chromium.org52424302012-08-29 15:14:30 +00001446 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001448 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001449 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001450 patchset = cl.GetMostRecentPatchset(issue)
1451 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001453 # Assume it's a URL to the patch. Default to https.
1454 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001455 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001456 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 DieWithError('Must pass an issue ID or full URL for '
1458 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001459 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001460 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001461 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462
1463 if options.newbranch:
1464 if options.force:
1465 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001466 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 RunGit(['checkout', '-b', options.newbranch,
1468 Changelist().GetUpstreamBranch()])
1469
1470 # Switch up to the top-level directory, if necessary, in preparation for
1471 # applying the patch.
1472 top = RunGit(['rev-parse', '--show-cdup']).strip()
1473 if top:
1474 os.chdir(top)
1475
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476 # Git patches have a/ at the beginning of source paths. We strip that out
1477 # with a sed script rather than the -p flag to patch so we can feed either
1478 # Git or svn-style patches into the same apply command.
1479 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001480 try:
1481 patch_data = subprocess2.check_output(
1482 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1483 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001484 DieWithError('Git patch mungling failed.')
1485 logging.info(patch_data)
1486 # We use "git apply" to apply the patch instead of "patch" so that we can
1487 # pick up file adds.
1488 # The --index flag means: also insert into the index (so we catch adds).
1489 cmd = ['git', 'apply', '--index', '-p0']
1490 if options.reject:
1491 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001492 try:
1493 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1494 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 DieWithError('Failed to apply the patch')
1496
1497 # If we had an issue, commit the current state and register the issue.
1498 if not options.nocommit:
1499 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1500 cl = Changelist()
1501 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001502 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503 print "Committed patch."
1504 else:
1505 print "Patch applied to index."
1506 return 0
1507
1508
1509def CMDrebase(parser, args):
1510 """rebase current branch on top of svn repo"""
1511 # Provide a wrapper for git svn rebase to help avoid accidental
1512 # git svn dcommit.
1513 # It's the only command that doesn't use parser at all since we just defer
1514 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001515 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516
1517
1518def GetTreeStatus():
1519 """Fetches the tree status and returns either 'open', 'closed',
1520 'unknown' or 'unset'."""
1521 url = settings.GetTreeStatusUrl(error_ok=True)
1522 if url:
1523 status = urllib2.urlopen(url).read().lower()
1524 if status.find('closed') != -1 or status == '0':
1525 return 'closed'
1526 elif status.find('open') != -1 or status == '1':
1527 return 'open'
1528 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529 return 'unset'
1530
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001531
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532def GetTreeStatusReason():
1533 """Fetches the tree status from a json url and returns the message
1534 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001535 url = settings.GetTreeStatusUrl()
1536 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537 connection = urllib2.urlopen(json_url)
1538 status = json.loads(connection.read())
1539 connection.close()
1540 return status['message']
1541
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001542
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543def CMDtree(parser, args):
1544 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001545 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546 status = GetTreeStatus()
1547 if 'unset' == status:
1548 print 'You must configure your tree status URL by running "git cl config".'
1549 return 2
1550
1551 print "The tree is %s" % status
1552 print
1553 print GetTreeStatusReason()
1554 if status != 'open':
1555 return 1
1556 return 0
1557
1558
maruel@chromium.org15192402012-09-06 12:38:29 +00001559def CMDtry(parser, args):
1560 """Triggers a try job through Rietveld."""
1561 group = optparse.OptionGroup(parser, "Try job options")
1562 group.add_option(
1563 "-b", "--bot", action="append",
1564 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1565 "times to specify multiple builders. ex: "
1566 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1567 "the try server waterfall for the builders name and the tests "
1568 "available. Can also be used to specify gtest_filter, e.g. "
1569 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1570 group.add_option(
1571 "-r", "--revision",
1572 help="Revision to use for the try job; default: the "
1573 "revision will be determined by the try server; see "
1574 "its waterfall for more info")
1575 group.add_option(
1576 "-c", "--clobber", action="store_true", default=False,
1577 help="Force a clobber before building; e.g. don't do an "
1578 "incremental build")
1579 group.add_option(
1580 "--project",
1581 help="Override which project to use. Projects are defined "
1582 "server-side to define what default bot set to use")
1583 group.add_option(
1584 "-t", "--testfilter", action="append", default=[],
1585 help=("Apply a testfilter to all the selected builders. Unless the "
1586 "builders configurations are similar, use multiple "
1587 "--bot <builder>:<test> arguments."))
1588 group.add_option(
1589 "-n", "--name", help="Try job name; default to current branch name")
1590 parser.add_option_group(group)
1591 options, args = parser.parse_args(args)
1592
1593 if args:
1594 parser.error('Unknown arguments: %s' % args)
1595
1596 cl = Changelist()
1597 if not cl.GetIssue():
1598 parser.error('Need to upload first')
1599
1600 if not options.name:
1601 options.name = cl.GetBranch()
1602
1603 # Process --bot and --testfilter.
1604 if not options.bot:
1605 # Get try slaves from PRESUBMIT.py files if not specified.
1606 change = cl.GetChange(cl.GetUpstreamBranch(), None)
1607 options.bot = presubmit_support.DoGetTrySlaves(
1608 change,
1609 change.LocalPaths(),
1610 settings.GetRoot(),
1611 None,
1612 None,
1613 options.verbose,
1614 sys.stdout)
1615 if not options.bot:
1616 parser.error('No default try builder to try, use --bot')
1617
1618 builders_and_tests = {}
1619 for bot in options.bot:
1620 if ':' in bot:
1621 builder, tests = bot.split(':', 1)
1622 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1623 elif ',' in bot:
1624 parser.error('Specify one bot per --bot flag')
1625 else:
1626 builders_and_tests.setdefault(bot, []).append('defaulttests')
1627
1628 if options.testfilter:
1629 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1630 builders_and_tests = dict(
1631 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1632 if t != ['compile'])
1633
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001634 if any('triggered' in b for b in builders_and_tests):
1635 print >> sys.stderr, (
1636 'ERROR You are trying to send a job to a triggered bot. This type of'
1637 ' bot requires an\ninitial job from a parent (usually a builder). '
1638 'Instead send your job to the parent.\n'
1639 'Bot list: %s' % builders_and_tests)
1640 return 1
1641
maruel@chromium.org15192402012-09-06 12:38:29 +00001642 patchset = cl.GetPatchset()
1643 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001644 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001645
1646 cl.RpcServer().trigger_try_jobs(
1647 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1648 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001649 print('Tried jobs on:')
1650 length = max(len(builder) for builder in builders_and_tests)
1651 for builder in sorted(builders_and_tests):
1652 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001653 return 0
1654
1655
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001656@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001657def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001658 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001659 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001660 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001661 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001662 return 0
1663
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001664 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001665 if args:
1666 # One arg means set upstream branch.
1667 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1668 cl = Changelist()
1669 print "Upstream branch set to " + cl.GetUpstreamBranch()
1670 else:
1671 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672 return 0
1673
1674
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001675def CMDset_commit(parser, args):
1676 """set the commit bit"""
1677 _, args = parser.parse_args(args)
1678 if args:
1679 parser.error('Unrecognized args: %s' % ' '.join(args))
1680 cl = Changelist()
1681 cl.SetFlag('commit', '1')
1682 return 0
1683
1684
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001685def Command(name):
1686 return getattr(sys.modules[__name__], 'CMD' + name, None)
1687
1688
1689def CMDhelp(parser, args):
1690 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001691 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692 if len(args) == 1:
1693 return main(args + ['--help'])
1694 parser.print_help()
1695 return 0
1696
1697
1698def GenUsage(parser, command):
1699 """Modify an OptParse object with the function's documentation."""
1700 obj = Command(command)
1701 more = getattr(obj, 'usage_more', '')
1702 if command == 'help':
1703 command = '<command>'
1704 else:
1705 # OptParser.description prefer nicely non-formatted strings.
1706 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1707 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1708
1709
1710def main(argv):
1711 """Doesn't parse the arguments here, just find the right subcommand to
1712 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001713 if sys.hexversion < 0x02060000:
1714 print >> sys.stderr, (
1715 '\nYour python version %s is unsupported, please upgrade.\n' %
1716 sys.version.split(' ', 1)[0])
1717 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001718 # Reload settings.
1719 global settings
1720 settings = Settings()
1721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 # Do it late so all commands are listed.
1723 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1724 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1725 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1726
1727 # Create the option parse and add --verbose support.
1728 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001729 parser.add_option(
1730 '-v', '--verbose', action='count', default=0,
1731 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732 old_parser_args = parser.parse_args
1733 def Parse(args):
1734 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001735 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001736 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001737 elif options.verbose:
1738 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739 else:
1740 logging.basicConfig(level=logging.WARNING)
1741 return options, args
1742 parser.parse_args = Parse
1743
1744 if argv:
1745 command = Command(argv[0])
1746 if command:
1747 # "fix" the usage and the description now that we know the subcommand.
1748 GenUsage(parser, argv[0])
1749 try:
1750 return command(parser, argv[1:])
1751 except urllib2.HTTPError, e:
1752 if e.code != 500:
1753 raise
1754 DieWithError(
1755 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1756 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1757
1758 # Not a known command. Default to help.
1759 GenUsage(parser, 'help')
1760 return CMDhelp(parser, argv)
1761
1762
1763if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001764 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 sys.exit(main(sys.argv[1:]))