blob: 6c7afe48f50d4ce1a42dfdd338b2f05ce1b7f357 [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
bauerb@chromium.org866276c2011-03-18 20:09:31 +000095def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
96 """Return the corresponding git ref if |base_url| together with |glob_spec|
97 matches the full |url|.
98
99 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
100 """
101 fetch_suburl, as_ref = glob_spec.split(':')
102 if allow_wildcards:
103 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
104 if glob_match:
105 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
106 # "branches/{472,597,648}/src:refs/remotes/svn/*".
107 branch_re = re.escape(base_url)
108 if glob_match.group(1):
109 branch_re += '/' + re.escape(glob_match.group(1))
110 wildcard = glob_match.group(2)
111 if wildcard == '*':
112 branch_re += '([^/]*)'
113 else:
114 # Escape and replace surrounding braces with parentheses and commas
115 # with pipe symbols.
116 wildcard = re.escape(wildcard)
117 wildcard = re.sub('^\\\\{', '(', wildcard)
118 wildcard = re.sub('\\\\,', '|', wildcard)
119 wildcard = re.sub('\\\\}$', ')', wildcard)
120 branch_re += wildcard
121 if glob_match.group(3):
122 branch_re += re.escape(glob_match.group(3))
123 match = re.match(branch_re, url)
124 if match:
125 return re.sub('\*$', match.group(1), as_ref)
126
127 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
128 if fetch_suburl:
129 full_url = base_url + '/' + fetch_suburl
130 else:
131 full_url = base_url
132 if full_url == url:
133 return as_ref
134 return None
135
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000137def print_stats(args):
138 """Prints statistics about the change to the user."""
139 # --no-ext-diff is broken in some versions of Git, so try to work around
140 # this by overriding the environment (but there is still a problem if the
141 # git config key "diff.external" is used).
142 env = os.environ.copy()
143 if 'GIT_EXTERNAL_DIFF' in env:
144 del env['GIT_EXTERNAL_DIFF']
145 return subprocess2.call(
iannucci@chromium.org1512ab62012-09-11 01:30:56 +0000146 ['git', 'diff', '--no-ext-diff', '--stat', '--find-copies-harder',
147 '-l100000'] + args, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000148
149
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150class Settings(object):
151 def __init__(self):
152 self.default_server = None
153 self.cc = None
154 self.root = None
155 self.is_git_svn = None
156 self.svn_branch = None
157 self.tree_status_url = None
158 self.viewvc_url = None
159 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000160 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000161
162 def LazyUpdateIfNeeded(self):
163 """Updates the settings from a codereview.settings file, if available."""
164 if not self.updated:
165 cr_settings_file = FindCodereviewSettingsFile()
166 if cr_settings_file:
167 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000168 self.updated = True
169 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000170 self.updated = True
171
172 def GetDefaultServerUrl(self, error_ok=False):
173 if not self.default_server:
174 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000175 self.default_server = gclient_utils.UpgradeToHttps(
176 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000177 if error_ok:
178 return self.default_server
179 if not self.default_server:
180 error_message = ('Could not find settings file. You must configure '
181 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000182 self.default_server = gclient_utils.UpgradeToHttps(
183 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000184 return self.default_server
185
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000186 def GetRoot(self):
187 if not self.root:
188 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
189 return self.root
190
191 def GetIsGitSvn(self):
192 """Return true if this repo looks like it's using git-svn."""
193 if self.is_git_svn is None:
194 # If you have any "svn-remote.*" config keys, we think you're using svn.
195 self.is_git_svn = RunGitWithCode(
196 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
197 return self.is_git_svn
198
199 def GetSVNBranch(self):
200 if self.svn_branch is None:
201 if not self.GetIsGitSvn():
202 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
203
204 # Try to figure out which remote branch we're based on.
205 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000206 # 1) iterate through our branch history and find the svn URL.
207 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000208
209 # regexp matching the git-svn line that contains the URL.
210 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
211
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000212 # We don't want to go through all of history, so read a line from the
213 # pipe at a time.
214 # The -100 is an arbitrary limit so we don't search forever.
215 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000216 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000217 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000218 for line in proc.stdout:
219 match = git_svn_re.match(line)
220 if match:
221 url = match.group(1)
222 proc.stdout.close() # Cut pipe.
223 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000224
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000225 if url:
226 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
227 remotes = RunGit(['config', '--get-regexp',
228 r'^svn-remote\..*\.url']).splitlines()
229 for remote in remotes:
230 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000231 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000232 remote = match.group(1)
233 base_url = match.group(2)
234 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000235 ['config', 'svn-remote.%s.fetch' % remote],
236 error_ok=True).strip()
237 if fetch_spec:
238 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
239 if self.svn_branch:
240 break
241 branch_spec = RunGit(
242 ['config', 'svn-remote.%s.branches' % remote],
243 error_ok=True).strip()
244 if branch_spec:
245 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
246 if self.svn_branch:
247 break
248 tag_spec = RunGit(
249 ['config', 'svn-remote.%s.tags' % remote],
250 error_ok=True).strip()
251 if tag_spec:
252 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
253 if self.svn_branch:
254 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000255
256 if not self.svn_branch:
257 DieWithError('Can\'t guess svn branch -- try specifying it on the '
258 'command line')
259
260 return self.svn_branch
261
262 def GetTreeStatusUrl(self, error_ok=False):
263 if not self.tree_status_url:
264 error_message = ('You must configure your tree status URL by running '
265 '"git cl config".')
266 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
267 error_ok=error_ok,
268 error_message=error_message)
269 return self.tree_status_url
270
271 def GetViewVCUrl(self):
272 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000273 self.viewvc_url = gclient_utils.UpgradeToHttps(
274 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 return self.viewvc_url
276
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000277 def GetDefaultCCList(self):
278 return self._GetConfig('rietveld.cc', error_ok=True)
279
ukai@chromium.orge8077812012-02-03 03:41:46 +0000280 def GetIsGerrit(self):
281 """Return true if this repo is assosiated with gerrit code review system."""
282 if self.is_gerrit is None:
283 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
284 return self.is_gerrit
285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286 def _GetConfig(self, param, **kwargs):
287 self.LazyUpdateIfNeeded()
288 return RunGit(['config', param], **kwargs).strip()
289
290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000291def ShortBranchName(branch):
292 """Convert a name like 'refs/heads/foo' to just 'foo'."""
293 return branch.replace('refs/heads/', '')
294
295
296class Changelist(object):
297 def __init__(self, branchref=None):
298 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000299 global settings
300 if not settings:
301 # Happens when git_cl.py is used as a utility library.
302 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303 settings.GetDefaultServerUrl()
304 self.branchref = branchref
305 if self.branchref:
306 self.branch = ShortBranchName(self.branchref)
307 else:
308 self.branch = None
309 self.rietveld_server = None
310 self.upstream_branch = None
311 self.has_issue = False
312 self.issue = None
313 self.has_description = False
314 self.description = None
315 self.has_patchset = False
316 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000317 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000318 self.cc = None
319 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000320 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000321
322 def GetCCList(self):
323 """Return the users cc'd on this CL.
324
325 Return is a string suitable for passing to gcl with the --cc flag.
326 """
327 if self.cc is None:
328 base_cc = settings .GetDefaultCCList()
329 more_cc = ','.join(self.watchers)
330 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
331 return self.cc
332
333 def SetWatchers(self, watchers):
334 """Set the list of email addresses that should be cc'd based on the changed
335 files in this CL.
336 """
337 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000338
339 def GetBranch(self):
340 """Returns the short branch name, e.g. 'master'."""
341 if not self.branch:
342 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
343 self.branch = ShortBranchName(self.branchref)
344 return self.branch
345
346 def GetBranchRef(self):
347 """Returns the full branch name, e.g. 'refs/heads/master'."""
348 self.GetBranch() # Poke the lazy loader.
349 return self.branchref
350
351 def FetchUpstreamTuple(self):
352 """Returns a tuple containg remote and remote ref,
353 e.g. 'origin', 'refs/heads/master'
354 """
355 remote = '.'
356 branch = self.GetBranch()
357 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
358 error_ok=True).strip()
359 if upstream_branch:
360 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
361 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
363 error_ok=True).strip()
364 if upstream_branch:
365 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000366 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000367 # Fall back on trying a git-svn upstream branch.
368 if settings.GetIsGitSvn():
369 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000370 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000371 # Else, try to guess the origin remote.
372 remote_branches = RunGit(['branch', '-r']).split()
373 if 'origin/master' in remote_branches:
374 # Fall back on origin/master if it exits.
375 remote = 'origin'
376 upstream_branch = 'refs/heads/master'
377 elif 'origin/trunk' in remote_branches:
378 # Fall back on origin/trunk if it exists. Generally a shared
379 # git-svn clone
380 remote = 'origin'
381 upstream_branch = 'refs/heads/trunk'
382 else:
383 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000384Either pass complete "git diff"-style arguments, like
385 git cl upload origin/master
386or verify this branch is set up to track another (via the --track argument to
387"git checkout -b ...").""")
388
389 return remote, upstream_branch
390
391 def GetUpstreamBranch(self):
392 if self.upstream_branch is None:
393 remote, upstream_branch = self.FetchUpstreamTuple()
394 if remote is not '.':
395 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
396 self.upstream_branch = upstream_branch
397 return self.upstream_branch
398
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000399 def GetRemote(self):
400 if not self._remote:
401 self._remote = self.FetchUpstreamTuple()[0]
402 if self._remote == '.':
403
404 remotes = RunGit(['remote'], error_ok=True).split()
405 if len(remotes) == 1:
406 self._remote, = remotes
407 elif 'origin' in remotes:
408 self._remote = 'origin'
409 logging.warning('Could not determine which remote this change is '
410 'associated with, so defaulting to "%s". This may '
411 'not be what you want. You may prevent this message '
412 'by running "git svn info" as documented here: %s',
413 self._remote,
414 GIT_INSTRUCTIONS_URL)
415 else:
416 logging.warn('Could not determine which remote this change is '
417 'associated with. You may prevent this message by '
418 'running "git svn info" as documented here: %s',
419 GIT_INSTRUCTIONS_URL)
420 return self._remote
421
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000422 def GetGitBaseUrlFromConfig(self):
423 """Return the configured base URL from branch.<branchname>.baseurl.
424
425 Returns None if it is not set.
426 """
427 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
428 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000429
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 def GetRemoteUrl(self):
431 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
432
433 Returns None if there is no remote.
434 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000435 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
437
438 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000439 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
442 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000443 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000444 else:
445 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 self.has_issue = True
447 return self.issue
448
449 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000450 if not self.rietveld_server:
451 # If we're on a branch then get the server potentially associated
452 # with that branch.
453 if self.GetIssue():
454 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
455 ['config', self._RietveldServer()], error_ok=True).strip())
456 if not self.rietveld_server:
457 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 return self.rietveld_server
459
460 def GetIssueURL(self):
461 """Get the URL for a particular issue."""
462 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
463
464 def GetDescription(self, pretty=False):
465 if not self.has_description:
466 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000467 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000468 try:
469 self.description = self.RpcServer().get_description(issue).strip()
470 except urllib2.HTTPError, e:
471 if e.code == 404:
472 DieWithError(
473 ('\nWhile fetching the description for issue %d, received a '
474 '404 (not found)\n'
475 'error. It is likely that you deleted this '
476 'issue on the server. If this is the\n'
477 'case, please run\n\n'
478 ' git cl issue 0\n\n'
479 'to clear the association with the deleted issue. Then run '
480 'this command again.') % issue)
481 else:
482 DieWithError(
483 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 self.has_description = True
485 if pretty:
486 wrapper = textwrap.TextWrapper()
487 wrapper.initial_indent = wrapper.subsequent_indent = ' '
488 return wrapper.fill(self.description)
489 return self.description
490
491 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000492 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 if not self.has_patchset:
494 patchset = RunGit(['config', self._PatchsetSetting()],
495 error_ok=True).strip()
496 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000497 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000498 else:
499 self.patchset = None
500 self.has_patchset = True
501 return self.patchset
502
503 def SetPatchset(self, patchset):
504 """Set this branch's patchset. If patchset=0, clears the patchset."""
505 if patchset:
506 RunGit(['config', self._PatchsetSetting(), str(patchset)])
507 else:
508 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000509 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000510 self.has_patchset = False
511
binji@chromium.org0281f522012-09-14 13:37:59 +0000512 def GetMostRecentPatchset(self, issue):
513 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000514 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000515
516 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000517 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000518 '/download/issue%s_%s.diff' % (issue, patchset))
519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000520 def SetIssue(self, issue):
521 """Set this branch's issue. If issue=0, clears the issue."""
522 if issue:
523 RunGit(['config', self._IssueSetting(), str(issue)])
524 if self.rietveld_server:
525 RunGit(['config', self._RietveldServer(), self.rietveld_server])
526 else:
527 RunGit(['config', '--unset', self._IssueSetting()])
528 self.SetPatchset(0)
529 self.has_issue = False
530
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000531 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000532 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
533 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000534
535 # We use the sha1 of HEAD as a name of this change.
536 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000537 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000538 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000539 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000540 except subprocess2.CalledProcessError:
541 DieWithError(
542 ('\nFailed to diff against upstream branch %s!\n\n'
543 'This branch probably doesn\'t exist anymore. To reset the\n'
544 'tracking branch, please run\n'
545 ' git branch --set-upstream %s trunk\n'
546 'replacing trunk with origin/master or the relevant branch') %
547 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000548
maruel@chromium.org52424302012-08-29 15:14:30 +0000549 issue = self.GetIssue()
550 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000551 if issue:
552 description = self.GetDescription()
553 else:
554 # If the change was never uploaded, use the log messages of all commits
555 # up to the branch point, as git cl upload will prefill the description
556 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000557 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
558 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000559
560 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000561 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000562 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000563 name,
564 description,
565 absroot,
566 files,
567 issue,
568 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000569 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000570
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000571 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
572 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
573 change = self.GetChange(upstream_branch, author)
574
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000575 # Apply watchlists on upload.
576 if not committing:
577 watchlist = watchlists.Watchlists(change.RepositoryRoot())
578 files = [f.LocalPath() for f in change.AffectedFiles()]
579 self.SetWatchers(watchlist.GetWatchersForPaths(files))
580
581 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000582 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000583 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000584 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000585 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000586 except presubmit_support.PresubmitFailure, e:
587 DieWithError(
588 ('%s\nMaybe your depot_tools is out of date?\n'
589 'If all fails, contact maruel@') % e)
590
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000592 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000593 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000594 self.RpcServer().update_description(issue, self.description)
595 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000597 def SetFlag(self, flag, value):
598 """Patchset must match."""
599 if not self.GetPatchset():
600 DieWithError('The patchset needs to match. Send another patchset.')
601 try:
602 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000603 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000604 except urllib2.HTTPError, e:
605 if e.code == 404:
606 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
607 if e.code == 403:
608 DieWithError(
609 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
610 'match?') % (self.GetIssue(), self.GetPatchset()))
611 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000613 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 """Returns an upload.RpcServer() to access this review's rietveld instance.
615 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000616 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000617 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
618 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000619 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620
621 def _IssueSetting(self):
622 """Return the git setting that stores this change's issue."""
623 return 'branch.%s.rietveldissue' % self.GetBranch()
624
625 def _PatchsetSetting(self):
626 """Return the git setting that stores this change's most recent patchset."""
627 return 'branch.%s.rietveldpatchset' % self.GetBranch()
628
629 def _RietveldServer(self):
630 """Returns the git setting that stores this change's rietveld server."""
631 return 'branch.%s.rietveldserver' % self.GetBranch()
632
633
634def GetCodereviewSettingsInteractively():
635 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000636 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 server = settings.GetDefaultServerUrl(error_ok=True)
638 prompt = 'Rietveld server (host[:port])'
639 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000640 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641 if not server and not newserver:
642 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000643 if newserver:
644 newserver = gclient_utils.UpgradeToHttps(newserver)
645 if newserver != server:
646 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000648 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 prompt = caption
650 if initial:
651 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000652 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653 if new_val == 'x':
654 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000655 elif new_val:
656 if is_url:
657 new_val = gclient_utils.UpgradeToHttps(new_val)
658 if new_val != initial:
659 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000661 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000663 'tree-status-url', False)
664 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665
666 # TODO: configure a default branch to diff against, rather than this
667 # svn-based hackery.
668
669
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000670class ChangeDescription(object):
671 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000672 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000673 self.log_desc = log_desc
674 self.reviewers = reviewers
675 self.description = self.log_desc
676
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000677 def Prompt(self):
678 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000679# This will displayed on the codereview site.
680# The first line will also be used as the subject of the review.
681"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000682 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000683 if ('\nR=' not in self.description and
684 '\nTBR=' not in self.description and
685 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000686 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000687 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000688 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000689 content = content.rstrip('\n') + '\n'
690 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000691 if not content:
692 DieWithError('Running editor failed')
693 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000694 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000695 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000696 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000697
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000698 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000699 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000700 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000701 # Retrieves all reviewer lines
702 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000703 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000704 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000705 if reviewers:
706 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000707
708 def IsEmpty(self):
709 return not self.description
710
711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712def FindCodereviewSettingsFile(filename='codereview.settings'):
713 """Finds the given file starting in the cwd and going up.
714
715 Only looks up to the top of the repository unless an
716 'inherit-review-settings-ok' file exists in the root of the repository.
717 """
718 inherit_ok_file = 'inherit-review-settings-ok'
719 cwd = os.getcwd()
720 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
721 if os.path.isfile(os.path.join(root, inherit_ok_file)):
722 root = '/'
723 while True:
724 if filename in os.listdir(cwd):
725 if os.path.isfile(os.path.join(cwd, filename)):
726 return open(os.path.join(cwd, filename))
727 if cwd == root:
728 break
729 cwd = os.path.dirname(cwd)
730
731
732def LoadCodereviewSettingsFromFile(fileobj):
733 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000734 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 def SetProperty(name, setting, unset_error_ok=False):
737 fullname = 'rietveld.' + name
738 if setting in keyvals:
739 RunGit(['config', fullname, keyvals[setting]])
740 else:
741 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
742
743 SetProperty('server', 'CODE_REVIEW_SERVER')
744 # Only server setting is required. Other settings can be absent.
745 # In that case, we ignore errors raised during option deletion attempt.
746 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
747 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
748 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
749
ukai@chromium.orge8077812012-02-03 03:41:46 +0000750 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
751 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
752 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000753
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
755 #should be of the form
756 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
757 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
758 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
759 keyvals['ORIGIN_URL_CONFIG']])
760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000762def urlretrieve(source, destination):
763 """urllib is broken for SSL connections via a proxy therefore we
764 can't use urllib.urlretrieve()."""
765 with open(destination, 'w') as f:
766 f.write(urllib2.urlopen(source).read())
767
768
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000769def DownloadHooks(force):
770 """downloads hooks
771
772 Args:
773 force: True to update hooks. False to install hooks if not present.
774 """
775 if not settings.GetIsGerrit():
776 return
777 server_url = settings.GetDefaultServerUrl()
778 src = '%s/tools/hooks/commit-msg' % server_url
779 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
780 if not os.access(dst, os.X_OK):
781 if os.path.exists(dst):
782 if not force:
783 return
784 os.remove(dst)
785 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000786 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000787 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
788 except Exception:
789 if os.path.exists(dst):
790 os.remove(dst)
791 DieWithError('\nFailed to download hooks from %s' % src)
792
793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794@usage('[repo root containing codereview.settings]')
795def CMDconfig(parser, args):
796 """edit configuration for this tree"""
797
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000798 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799 if len(args) == 0:
800 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000801 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 return 0
803
804 url = args[0]
805 if not url.endswith('codereview.settings'):
806 url = os.path.join(url, 'codereview.settings')
807
808 # Load code review settings and download hooks (if available).
809 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000810 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 return 0
812
813
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000814def CMDbaseurl(parser, args):
815 """get or set base-url for this branch"""
816 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
817 branch = ShortBranchName(branchref)
818 _, args = parser.parse_args(args)
819 if not args:
820 print("Current base-url:")
821 return RunGit(['config', 'branch.%s.base-url' % branch],
822 error_ok=False).strip()
823 else:
824 print("Setting base-url to %s" % args[0])
825 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
826 error_ok=False).strip()
827
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829def CMDstatus(parser, args):
830 """show status of changelists"""
831 parser.add_option('--field',
832 help='print only specific field (desc|id|patch|url)')
833 (options, args) = parser.parse_args(args)
834
835 # TODO: maybe make show_branches a flag if necessary.
836 show_branches = not options.field
837
838 if show_branches:
839 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
840 if branches:
841 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000842 changes = (Changelist(branchref=b) for b in branches.splitlines())
843 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
844 alignment = max(5, max(len(b) for b in branches))
845 for branch in sorted(branches):
846 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
848 cl = Changelist()
849 if options.field:
850 if options.field.startswith('desc'):
851 print cl.GetDescription()
852 elif options.field == 'id':
853 issueid = cl.GetIssue()
854 if issueid:
855 print issueid
856 elif options.field == 'patch':
857 patchset = cl.GetPatchset()
858 if patchset:
859 print patchset
860 elif options.field == 'url':
861 url = cl.GetIssueURL()
862 if url:
863 print url
864 else:
865 print
866 print 'Current branch:',
867 if not cl.GetIssue():
868 print 'no issue assigned.'
869 return 0
870 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000871 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 print 'Issue description:'
873 print cl.GetDescription(pretty=True)
874 return 0
875
876
877@usage('[issue_number]')
878def CMDissue(parser, args):
879 """Set or display the current code review issue number.
880
881 Pass issue number 0 to clear the current issue.
882"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000883 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884
885 cl = Changelist()
886 if len(args) > 0:
887 try:
888 issue = int(args[0])
889 except ValueError:
890 DieWithError('Pass a number to set the issue or none to list it.\n'
891 'Maybe you want to run git cl status?')
892 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +0000893 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894 return 0
895
896
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000897def CMDcomments(parser, args):
898 """show review comments of the current changelist"""
899 (_, args) = parser.parse_args(args)
900 if args:
901 parser.error('Unsupported argument: %s' % args)
902
903 cl = Changelist()
904 if cl.GetIssue():
905 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
906 for message in sorted(data['messages'], key=lambda x: x['date']):
907 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
908 if message['text'].strip():
909 print '\n'.join(' ' + l for l in message['text'].splitlines())
910 return 0
911
912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913def CreateDescriptionFromLog(args):
914 """Pulls out the commit log to use as a base for the CL description."""
915 log_args = []
916 if len(args) == 1 and not args[0].endswith('.'):
917 log_args = [args[0] + '..']
918 elif len(args) == 1 and args[0].endswith('...'):
919 log_args = [args[0][:-1]]
920 elif len(args) == 2:
921 log_args = [args[0] + '..' + args[1]]
922 else:
923 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000924 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925
926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927def CMDpresubmit(parser, args):
928 """run presubmit tests on the current changelist"""
929 parser.add_option('--upload', action='store_true',
930 help='Run upload hook instead of the push/dcommit hook')
sbc@chromium.org495ad152012-09-04 23:07:42 +0000931 parser.add_option('--force', action='store_true',
932 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933 (options, args) = parser.parse_args(args)
934
935 # Make sure index is up-to-date before running diff-index.
936 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
sbc@chromium.org495ad152012-09-04 23:07:42 +0000937 if not options.force and RunGit(['diff-index', 'HEAD']):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 # TODO(maruel): Is this really necessary?
sbc@chromium.org495ad152012-09-04 23:07:42 +0000939 print ('Cannot presubmit with a dirty tree.\n'
940 'You must commit locally first (or use --force).')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941 return 1
942
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000943 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000944 if args:
945 base_branch = args[0]
946 else:
947 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000948 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000949
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000950 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000951 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000952 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000953 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954
955
ukai@chromium.orge8077812012-02-03 03:41:46 +0000956def GerritUpload(options, args, cl):
957 """upload the current branch to gerrit."""
958 # We assume the remote called "origin" is the one we want.
959 # It is probably not worthwhile to support different workflows.
960 remote = 'origin'
961 branch = 'master'
962 if options.target_branch:
963 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000964
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000965 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000966 if options.reviewers:
967 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000968 change_desc = ChangeDescription(log_desc, options.reviewers)
969 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000970 if change_desc.IsEmpty():
971 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 return 1
973
ukai@chromium.orge8077812012-02-03 03:41:46 +0000974 receive_options = []
975 cc = cl.GetCCList().split(',')
976 if options.cc:
977 cc += options.cc.split(',')
978 cc = filter(None, cc)
979 if cc:
980 receive_options += ['--cc=' + email for email in cc]
981 if change_desc.reviewers:
982 reviewers = filter(None, change_desc.reviewers.split(','))
983 if reviewers:
984 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000985
ukai@chromium.orge8077812012-02-03 03:41:46 +0000986 git_command = ['push']
987 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000988 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000989 ' '.join(receive_options))
990 git_command += [remote, 'HEAD:refs/for/' + branch]
991 RunGit(git_command)
992 # TODO(ukai): parse Change-Id: and set issue number?
993 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995
ukai@chromium.orge8077812012-02-03 03:41:46 +0000996def RietveldUpload(options, args, cl):
997 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 upload_args = ['--assume_yes'] # Don't ask about untracked files.
999 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 if options.emulate_svn_auto_props:
1001 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002
1003 change_desc = None
1004
1005 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001006 if options.title:
1007 upload_args.extend(['--title', options.title])
1008 elif options.message:
1009 # TODO(rogerta): for now, the -m option will also set the --title option
1010 # for upload.py. Soon this will be changed to set the --message option.
1011 # Will wait until people are used to typing -t instead of -m.
1012 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001013 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 print ("This branch is associated with issue %s. "
1015 "Adding patch to that issue." % cl.GetIssue())
1016 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001017 if options.title:
1018 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001019 message = options.message or CreateDescriptionFromLog(args)
1020 change_desc = ChangeDescription(message, options.reviewers)
1021 if not options.force:
1022 change_desc.Prompt()
1023 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001024
1025 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 print "Description is empty; aborting."
1027 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001028
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001029 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001030 if change_desc.reviewers:
1031 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001032 if options.send_mail:
1033 if not change_desc.reviewers:
1034 DieWithError("Must specify reviewers to send email.")
1035 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001036 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001037 if cc:
1038 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039
1040 # Include the upstream repo's URL in the change -- this is useful for
1041 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001042 remote_url = cl.GetGitBaseUrlFromConfig()
1043 if not remote_url:
1044 if settings.GetIsGitSvn():
1045 # URL is dependent on the current directory.
1046 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1047 if data:
1048 keys = dict(line.split(': ', 1) for line in data.splitlines()
1049 if ': ' in line)
1050 remote_url = keys.get('URL', None)
1051 else:
1052 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1053 remote_url = (cl.GetRemoteUrl() + '@'
1054 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 if remote_url:
1056 upload_args.extend(['--base_url', remote_url])
1057
1058 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001059 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001060 except KeyboardInterrupt:
1061 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062 except:
1063 # If we got an exception after the user typed a description for their
1064 # change, back up the description before re-raising.
1065 if change_desc:
1066 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1067 print '\nGot exception while uploading -- saving description to %s\n' \
1068 % backup_path
1069 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001070 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 backup_file.close()
1072 raise
1073
1074 if not cl.GetIssue():
1075 cl.SetIssue(issue)
1076 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001077
1078 if options.use_commit_queue:
1079 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 return 0
1081
1082
ukai@chromium.orge8077812012-02-03 03:41:46 +00001083@usage('[args to "git diff"]')
1084def CMDupload(parser, args):
1085 """upload the current changelist to codereview"""
1086 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1087 help='bypass upload presubmit hook')
1088 parser.add_option('-f', action='store_true', dest='force',
1089 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001090 parser.add_option('-m', dest='message', help='message for patchset')
1091 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001092 parser.add_option('-r', '--reviewers',
1093 help='reviewer email addresses')
1094 parser.add_option('--cc',
1095 help='cc email addresses')
1096 parser.add_option('--send-mail', action='store_true',
1097 help='send email to reviewer immediately')
1098 parser.add_option("--emulate_svn_auto_props", action="store_true",
1099 dest="emulate_svn_auto_props",
1100 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001101 parser.add_option('-c', '--use-commit-queue', action='store_true',
1102 help='tell the commit queue to commit this patchset')
1103 if settings.GetIsGerrit():
1104 parser.add_option('--target_branch', dest='target_branch', default='master',
1105 help='target branch to upload')
1106 (options, args) = parser.parse_args(args)
1107
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001108 # Print warning if the user used the -m/--message argument. This will soon
1109 # change to -t/--title.
1110 if options.message:
1111 print >> sys.stderr, (
1112 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1113 'In the near future, -m or --message will send a message instead.\n'
1114 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001115
ukai@chromium.orge8077812012-02-03 03:41:46 +00001116 # Make sure index is up-to-date before running diff-index.
1117 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1118 if RunGit(['diff-index', 'HEAD']):
1119 print 'Cannot upload with a dirty tree. You must commit locally first.'
1120 return 1
1121
1122 cl = Changelist()
1123 if args:
1124 # TODO(ukai): is it ok for gerrit case?
1125 base_branch = args[0]
1126 else:
1127 # Default to diffing against the "upstream" branch.
1128 base_branch = cl.GetUpstreamBranch()
1129 args = [base_branch + "..."]
1130
1131 if not options.bypass_hooks:
1132 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1133 may_prompt=not options.force,
1134 verbose=options.verbose,
1135 author=None)
1136 if not hook_results.should_continue():
1137 return 1
1138 if not options.reviewers and hook_results.reviewers:
1139 options.reviewers = hook_results.reviewers
1140
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001141 print_stats(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001142 if settings.GetIsGerrit():
1143 return GerritUpload(options, args, cl)
1144 return RietveldUpload(options, args, cl)
1145
1146
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001147def IsSubmoduleMergeCommit(ref):
1148 # When submodules are added to the repo, we expect there to be a single
1149 # non-git-svn merge commit at remote HEAD with a signature comment.
1150 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001151 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001152 return RunGit(cmd) != ''
1153
1154
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155def SendUpstream(parser, args, cmd):
1156 """Common code for CmdPush and CmdDCommit
1157
1158 Squashed commit into a single.
1159 Updates changelog with metadata (e.g. pointer to review).
1160 Pushes/dcommits the code upstream.
1161 Updates review and closes.
1162 """
1163 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1164 help='bypass upload presubmit hook')
1165 parser.add_option('-m', dest='message',
1166 help="override review description")
1167 parser.add_option('-f', action='store_true', dest='force',
1168 help="force yes to questions (don't prompt)")
1169 parser.add_option('-c', dest='contributor',
1170 help="external contributor for patch (appended to " +
1171 "description and used as author for git). Should be " +
1172 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 (options, args) = parser.parse_args(args)
1174 cl = Changelist()
1175
1176 if not args or cmd == 'push':
1177 # Default to merging against our best guess of the upstream branch.
1178 args = [cl.GetUpstreamBranch()]
1179
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001180 if options.contributor:
1181 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1182 print "Please provide contibutor as 'First Last <email@example.com>'"
1183 return 1
1184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001186 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001188 # Make sure index is up-to-date before running diff-index.
1189 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 if RunGit(['diff-index', 'HEAD']):
1191 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1192 return 1
1193
1194 # This rev-list syntax means "show all commits not in my branch that
1195 # are in base_branch".
1196 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1197 base_branch]).splitlines()
1198 if upstream_commits:
1199 print ('Base branch "%s" has %d commits '
1200 'not in this branch.' % (base_branch, len(upstream_commits)))
1201 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1202 return 1
1203
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001204 # This is the revision `svn dcommit` will commit on top of.
1205 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1206 '--pretty=format:%H'])
1207
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001209 # If the base_head is a submodule merge commit, the first parent of the
1210 # base_head should be a git-svn commit, which is what we're interested in.
1211 base_svn_head = base_branch
1212 if base_has_submodules:
1213 base_svn_head += '^1'
1214
1215 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216 if extra_commits:
1217 print ('This branch has %d additional commits not upstreamed yet.'
1218 % len(extra_commits.splitlines()))
1219 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1220 'before attempting to %s.' % (base_branch, cmd))
1221 return 1
1222
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001223 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001224 author = None
1225 if options.contributor:
1226 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001227 hook_results = cl.RunHook(
1228 committing=True,
1229 upstream_branch=base_branch,
1230 may_prompt=not options.force,
1231 verbose=options.verbose,
1232 author=author)
1233 if not hook_results.should_continue():
1234 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
1236 if cmd == 'dcommit':
1237 # Check the tree status if the tree status URL is set.
1238 status = GetTreeStatus()
1239 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001240 print('The tree is closed. Please wait for it to reopen. Use '
1241 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 return 1
1243 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001244 print('Unable to determine tree status. Please verify manually and '
1245 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001246 else:
1247 breakpad.SendStack(
1248 'GitClHooksBypassedCommit',
1249 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001250 (cl.GetRietveldServer(), cl.GetIssue()),
1251 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252
1253 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001254 if not description and cl.GetIssue():
1255 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001257 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001258 if not cl.GetIssue() and options.bypass_hooks:
1259 description = CreateDescriptionFromLog([base_branch])
1260 else:
1261 print 'No description set.'
1262 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1263 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001265 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267
1268 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 description += "\nPatch from %s." % options.contributor
1270 print 'Description:', repr(description)
1271
1272 branches = [base_branch, cl.GetBranchRef()]
1273 if not options.force:
maruel@chromium.org49e3d802012-07-18 23:54:45 +00001274 print_stats(branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001275 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001277 # We want to squash all this branch's commits into one commit with the proper
1278 # description. We do this by doing a "reset --soft" to the base branch (which
1279 # keeps the working copy the same), then dcommitting that. If origin/master
1280 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1281 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001283 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1284 # Delete the branches if they exist.
1285 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1286 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1287 result = RunGitWithCode(showref_cmd)
1288 if result[0] == 0:
1289 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290
1291 # We might be in a directory that's present in this branch but not in the
1292 # trunk. Move up to the top of the tree so that git commands that expect a
1293 # valid CWD won't fail after we check out the merge branch.
1294 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1295 if rel_base_path:
1296 os.chdir(rel_base_path)
1297
1298 # Stuff our change into the merge branch.
1299 # We wrap in a try...finally block so if anything goes wrong,
1300 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001301 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001303 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1304 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 if options.contributor:
1306 RunGit(['commit', '--author', options.contributor, '-m', description])
1307 else:
1308 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001309 if base_has_submodules:
1310 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1311 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1312 RunGit(['checkout', CHERRY_PICK_BRANCH])
1313 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 if cmd == 'push':
1315 # push the merge branch.
1316 remote, branch = cl.FetchUpstreamTuple()
1317 retcode, output = RunGitWithCode(
1318 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1319 logging.debug(output)
1320 else:
1321 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001322 retcode, output = RunGitWithCode(['svn', 'dcommit',
1323 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324 finally:
1325 # And then swap back to the original branch and clean up.
1326 RunGit(['checkout', '-q', cl.GetBranch()])
1327 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001328 if base_has_submodules:
1329 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330
1331 if cl.GetIssue():
1332 if cmd == 'dcommit' and 'Committed r' in output:
1333 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1334 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001335 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1336 for l in output.splitlines(False))
1337 match = filter(None, match)
1338 if len(match) != 1:
1339 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1340 output)
1341 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342 else:
1343 return 1
1344 viewvc_url = settings.GetViewVCUrl()
1345 if viewvc_url and revision:
1346 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1347 print ('Closing issue '
1348 '(you may be prompted for your codereview password)...')
1349 cl.CloseIssue()
1350 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001351
1352 if retcode == 0:
1353 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1354 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001355 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001356
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 return 0
1358
1359
1360@usage('[upstream branch to apply against]')
1361def CMDdcommit(parser, args):
1362 """commit the current changelist via git-svn"""
1363 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001364 message = """This doesn't appear to be an SVN repository.
1365If your project has a git mirror with an upstream SVN master, you probably need
1366to run 'git svn init', see your project's git mirror documentation.
1367If your project has a true writeable upstream repository, you probably want
1368to run 'git cl push' instead.
1369Choose wisely, if you get this wrong, your commit might appear to succeed but
1370will instead be silently ignored."""
1371 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001372 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 return SendUpstream(parser, args, 'dcommit')
1374
1375
1376@usage('[upstream branch to apply against]')
1377def CMDpush(parser, args):
1378 """commit the current changelist via git"""
1379 if settings.GetIsGitSvn():
1380 print('This appears to be an SVN repository.')
1381 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001382 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383 return SendUpstream(parser, args, 'push')
1384
1385
1386@usage('<patch url or issue id>')
1387def CMDpatch(parser, args):
1388 """patch in a code review"""
1389 parser.add_option('-b', dest='newbranch',
1390 help='create a new branch off trunk for the patch')
1391 parser.add_option('-f', action='store_true', dest='force',
1392 help='with -b, clobber any existing branch')
1393 parser.add_option('--reject', action='store_true', dest='reject',
1394 help='allow failed patches and spew .rej files')
1395 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1396 help="don't commit after patch applies")
1397 (options, args) = parser.parse_args(args)
1398 if len(args) != 1:
1399 parser.print_help()
1400 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001401 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001403 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001404 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001405
maruel@chromium.org52424302012-08-29 15:14:30 +00001406 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001408 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001409 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001410 patchset = cl.GetMostRecentPatchset(issue)
1411 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001413 # Assume it's a URL to the patch. Default to https.
1414 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001415 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001416 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 DieWithError('Must pass an issue ID or full URL for '
1418 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001419 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001420 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001421 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
1423 if options.newbranch:
1424 if options.force:
1425 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001426 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 RunGit(['checkout', '-b', options.newbranch,
1428 Changelist().GetUpstreamBranch()])
1429
1430 # Switch up to the top-level directory, if necessary, in preparation for
1431 # applying the patch.
1432 top = RunGit(['rev-parse', '--show-cdup']).strip()
1433 if top:
1434 os.chdir(top)
1435
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 # Git patches have a/ at the beginning of source paths. We strip that out
1437 # with a sed script rather than the -p flag to patch so we can feed either
1438 # Git or svn-style patches into the same apply command.
1439 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001440 try:
1441 patch_data = subprocess2.check_output(
1442 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1443 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 DieWithError('Git patch mungling failed.')
1445 logging.info(patch_data)
1446 # We use "git apply" to apply the patch instead of "patch" so that we can
1447 # pick up file adds.
1448 # The --index flag means: also insert into the index (so we catch adds).
1449 cmd = ['git', 'apply', '--index', '-p0']
1450 if options.reject:
1451 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001452 try:
1453 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1454 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455 DieWithError('Failed to apply the patch')
1456
1457 # If we had an issue, commit the current state and register the issue.
1458 if not options.nocommit:
1459 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1460 cl = Changelist()
1461 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001462 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 print "Committed patch."
1464 else:
1465 print "Patch applied to index."
1466 return 0
1467
1468
1469def CMDrebase(parser, args):
1470 """rebase current branch on top of svn repo"""
1471 # Provide a wrapper for git svn rebase to help avoid accidental
1472 # git svn dcommit.
1473 # It's the only command that doesn't use parser at all since we just defer
1474 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001475 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476
1477
1478def GetTreeStatus():
1479 """Fetches the tree status and returns either 'open', 'closed',
1480 'unknown' or 'unset'."""
1481 url = settings.GetTreeStatusUrl(error_ok=True)
1482 if url:
1483 status = urllib2.urlopen(url).read().lower()
1484 if status.find('closed') != -1 or status == '0':
1485 return 'closed'
1486 elif status.find('open') != -1 or status == '1':
1487 return 'open'
1488 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489 return 'unset'
1490
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001491
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492def GetTreeStatusReason():
1493 """Fetches the tree status from a json url and returns the message
1494 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001495 url = settings.GetTreeStatusUrl()
1496 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497 connection = urllib2.urlopen(json_url)
1498 status = json.loads(connection.read())
1499 connection.close()
1500 return status['message']
1501
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001502
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503def CMDtree(parser, args):
1504 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001505 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 status = GetTreeStatus()
1507 if 'unset' == status:
1508 print 'You must configure your tree status URL by running "git cl config".'
1509 return 2
1510
1511 print "The tree is %s" % status
1512 print
1513 print GetTreeStatusReason()
1514 if status != 'open':
1515 return 1
1516 return 0
1517
1518
maruel@chromium.org15192402012-09-06 12:38:29 +00001519def CMDtry(parser, args):
1520 """Triggers a try job through Rietveld."""
1521 group = optparse.OptionGroup(parser, "Try job options")
1522 group.add_option(
1523 "-b", "--bot", action="append",
1524 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1525 "times to specify multiple builders. ex: "
1526 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1527 "the try server waterfall for the builders name and the tests "
1528 "available. Can also be used to specify gtest_filter, e.g. "
1529 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1530 group.add_option(
1531 "-r", "--revision",
1532 help="Revision to use for the try job; default: the "
1533 "revision will be determined by the try server; see "
1534 "its waterfall for more info")
1535 group.add_option(
1536 "-c", "--clobber", action="store_true", default=False,
1537 help="Force a clobber before building; e.g. don't do an "
1538 "incremental build")
1539 group.add_option(
1540 "--project",
1541 help="Override which project to use. Projects are defined "
1542 "server-side to define what default bot set to use")
1543 group.add_option(
1544 "-t", "--testfilter", action="append", default=[],
1545 help=("Apply a testfilter to all the selected builders. Unless the "
1546 "builders configurations are similar, use multiple "
1547 "--bot <builder>:<test> arguments."))
1548 group.add_option(
1549 "-n", "--name", help="Try job name; default to current branch name")
1550 parser.add_option_group(group)
1551 options, args = parser.parse_args(args)
1552
1553 if args:
1554 parser.error('Unknown arguments: %s' % args)
1555
1556 cl = Changelist()
1557 if not cl.GetIssue():
1558 parser.error('Need to upload first')
1559
1560 if not options.name:
1561 options.name = cl.GetBranch()
1562
1563 # Process --bot and --testfilter.
1564 if not options.bot:
1565 # Get try slaves from PRESUBMIT.py files if not specified.
1566 change = cl.GetChange(cl.GetUpstreamBranch(), None)
1567 options.bot = presubmit_support.DoGetTrySlaves(
1568 change,
1569 change.LocalPaths(),
1570 settings.GetRoot(),
1571 None,
1572 None,
1573 options.verbose,
1574 sys.stdout)
1575 if not options.bot:
1576 parser.error('No default try builder to try, use --bot')
1577
1578 builders_and_tests = {}
1579 for bot in options.bot:
1580 if ':' in bot:
1581 builder, tests = bot.split(':', 1)
1582 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1583 elif ',' in bot:
1584 parser.error('Specify one bot per --bot flag')
1585 else:
1586 builders_and_tests.setdefault(bot, []).append('defaulttests')
1587
1588 if options.testfilter:
1589 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1590 builders_and_tests = dict(
1591 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1592 if t != ['compile'])
1593
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001594 if any('triggered' in b for b in builders_and_tests):
1595 print >> sys.stderr, (
1596 'ERROR You are trying to send a job to a triggered bot. This type of'
1597 ' bot requires an\ninitial job from a parent (usually a builder). '
1598 'Instead send your job to the parent.\n'
1599 'Bot list: %s' % builders_and_tests)
1600 return 1
1601
maruel@chromium.org15192402012-09-06 12:38:29 +00001602 patchset = cl.GetPatchset()
1603 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001604 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001605
1606 cl.RpcServer().trigger_try_jobs(
1607 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1608 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001609 print('Tried jobs on:')
1610 length = max(len(builder) for builder in builders_and_tests)
1611 for builder in sorted(builders_and_tests):
1612 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001613 return 0
1614
1615
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001616@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001617def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001618 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001619 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001620 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001621 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001622 return 0
1623
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001625 if args:
1626 # One arg means set upstream branch.
1627 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1628 cl = Changelist()
1629 print "Upstream branch set to " + cl.GetUpstreamBranch()
1630 else:
1631 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632 return 0
1633
1634
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001635def CMDset_commit(parser, args):
1636 """set the commit bit"""
1637 _, args = parser.parse_args(args)
1638 if args:
1639 parser.error('Unrecognized args: %s' % ' '.join(args))
1640 cl = Changelist()
1641 cl.SetFlag('commit', '1')
1642 return 0
1643
1644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645def Command(name):
1646 return getattr(sys.modules[__name__], 'CMD' + name, None)
1647
1648
1649def CMDhelp(parser, args):
1650 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001651 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001652 if len(args) == 1:
1653 return main(args + ['--help'])
1654 parser.print_help()
1655 return 0
1656
1657
1658def GenUsage(parser, command):
1659 """Modify an OptParse object with the function's documentation."""
1660 obj = Command(command)
1661 more = getattr(obj, 'usage_more', '')
1662 if command == 'help':
1663 command = '<command>'
1664 else:
1665 # OptParser.description prefer nicely non-formatted strings.
1666 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1667 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1668
1669
1670def main(argv):
1671 """Doesn't parse the arguments here, just find the right subcommand to
1672 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001673 if sys.hexversion < 0x02060000:
1674 print >> sys.stderr, (
1675 '\nYour python version %s is unsupported, please upgrade.\n' %
1676 sys.version.split(' ', 1)[0])
1677 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001678 # Reload settings.
1679 global settings
1680 settings = Settings()
1681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 # Do it late so all commands are listed.
1683 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1684 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1685 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1686
1687 # Create the option parse and add --verbose support.
1688 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001689 parser.add_option(
1690 '-v', '--verbose', action='count', default=0,
1691 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692 old_parser_args = parser.parse_args
1693 def Parse(args):
1694 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001695 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001696 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001697 elif options.verbose:
1698 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001699 else:
1700 logging.basicConfig(level=logging.WARNING)
1701 return options, args
1702 parser.parse_args = Parse
1703
1704 if argv:
1705 command = Command(argv[0])
1706 if command:
1707 # "fix" the usage and the description now that we know the subcommand.
1708 GenUsage(parser, argv[0])
1709 try:
1710 return command(parser, argv[1:])
1711 except urllib2.HTTPError, e:
1712 if e.code != 500:
1713 raise
1714 DieWithError(
1715 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1716 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1717
1718 # Not a known command. Default to help.
1719 GenUsage(parser, 'help')
1720 return CMDhelp(parser, argv)
1721
1722
1723if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001724 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 sys.exit(main(sys.argv[1:]))