blob: c332423ad471d3d9e473a6e9bc1339977c971aa9 [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000010import logging
11import optparse
12import os
13import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000014import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000017import urlparse
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import urllib
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
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000026try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000028except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000030 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031 except ImportError:
32 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000033 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000034 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035
36
37from third_party import upload
38import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000039import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000040import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000042import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000044import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import watchlists
46
47
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000048DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000049POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000051GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052
maruel@chromium.org90541732011-04-01 17:54:18 +000053
maruel@chromium.orgddd59412011-11-30 14:20:38 +000054# Initialized in main()
55settings = None
56
57
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000059 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060 sys.exit(1)
61
62
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000063def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000064 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065 return subprocess2.check_output(args, shell=False, **kwargs)
66 except subprocess2.CalledProcessError, e:
67 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000069 'Command "%s" failed.\n%s' % (
70 ' '.join(args), error_message or e.stdout or ''))
71 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
73
74def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000075 """Returns stdout."""
76 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000077
78
79def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000080 """Returns return code and stdout."""
81 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
82 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083
84
85def usage(more):
86 def hook(fn):
87 fn.usage_more = more
88 return fn
89 return hook
90
91
maruel@chromium.org90541732011-04-01 17:54:18 +000092def ask_for_data(prompt):
93 try:
94 return raw_input(prompt)
95 except KeyboardInterrupt:
96 # Hide the exception.
97 sys.exit(1)
98
99
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000100def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
101 """Return the corresponding git ref if |base_url| together with |glob_spec|
102 matches the full |url|.
103
104 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
105 """
106 fetch_suburl, as_ref = glob_spec.split(':')
107 if allow_wildcards:
108 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
109 if glob_match:
110 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
111 # "branches/{472,597,648}/src:refs/remotes/svn/*".
112 branch_re = re.escape(base_url)
113 if glob_match.group(1):
114 branch_re += '/' + re.escape(glob_match.group(1))
115 wildcard = glob_match.group(2)
116 if wildcard == '*':
117 branch_re += '([^/]*)'
118 else:
119 # Escape and replace surrounding braces with parentheses and commas
120 # with pipe symbols.
121 wildcard = re.escape(wildcard)
122 wildcard = re.sub('^\\\\{', '(', wildcard)
123 wildcard = re.sub('\\\\,', '|', wildcard)
124 wildcard = re.sub('\\\\}$', ')', wildcard)
125 branch_re += wildcard
126 if glob_match.group(3):
127 branch_re += re.escape(glob_match.group(3))
128 match = re.match(branch_re, url)
129 if match:
130 return re.sub('\*$', match.group(1), as_ref)
131
132 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
133 if fetch_suburl:
134 full_url = base_url + '/' + fetch_suburl
135 else:
136 full_url = base_url
137 if full_url == url:
138 return as_ref
139 return None
140
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000141
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000142class Settings(object):
143 def __init__(self):
144 self.default_server = None
145 self.cc = None
146 self.root = None
147 self.is_git_svn = None
148 self.svn_branch = None
149 self.tree_status_url = None
150 self.viewvc_url = None
151 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000152 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000153
154 def LazyUpdateIfNeeded(self):
155 """Updates the settings from a codereview.settings file, if available."""
156 if not self.updated:
157 cr_settings_file = FindCodereviewSettingsFile()
158 if cr_settings_file:
159 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000160 self.updated = True
161 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162 self.updated = True
163
164 def GetDefaultServerUrl(self, error_ok=False):
165 if not self.default_server:
166 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000167 self.default_server = gclient_utils.UpgradeToHttps(
168 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000169 if error_ok:
170 return self.default_server
171 if not self.default_server:
172 error_message = ('Could not find settings file. You must configure '
173 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000174 self.default_server = gclient_utils.UpgradeToHttps(
175 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000176 return self.default_server
177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000178 def GetRoot(self):
179 if not self.root:
180 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
181 return self.root
182
183 def GetIsGitSvn(self):
184 """Return true if this repo looks like it's using git-svn."""
185 if self.is_git_svn is None:
186 # If you have any "svn-remote.*" config keys, we think you're using svn.
187 self.is_git_svn = RunGitWithCode(
188 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
189 return self.is_git_svn
190
191 def GetSVNBranch(self):
192 if self.svn_branch is None:
193 if not self.GetIsGitSvn():
194 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
195
196 # Try to figure out which remote branch we're based on.
197 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000198 # 1) iterate through our branch history and find the svn URL.
199 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000200
201 # regexp matching the git-svn line that contains the URL.
202 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
203
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000204 # We don't want to go through all of history, so read a line from the
205 # pipe at a time.
206 # The -100 is an arbitrary limit so we don't search forever.
207 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000208 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000209 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000210 for line in proc.stdout:
211 match = git_svn_re.match(line)
212 if match:
213 url = match.group(1)
214 proc.stdout.close() # Cut pipe.
215 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000216
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000217 if url:
218 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
219 remotes = RunGit(['config', '--get-regexp',
220 r'^svn-remote\..*\.url']).splitlines()
221 for remote in remotes:
222 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000223 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000224 remote = match.group(1)
225 base_url = match.group(2)
226 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000227 ['config', 'svn-remote.%s.fetch' % remote],
228 error_ok=True).strip()
229 if fetch_spec:
230 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
231 if self.svn_branch:
232 break
233 branch_spec = RunGit(
234 ['config', 'svn-remote.%s.branches' % remote],
235 error_ok=True).strip()
236 if branch_spec:
237 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
238 if self.svn_branch:
239 break
240 tag_spec = RunGit(
241 ['config', 'svn-remote.%s.tags' % remote],
242 error_ok=True).strip()
243 if tag_spec:
244 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
245 if self.svn_branch:
246 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000247
248 if not self.svn_branch:
249 DieWithError('Can\'t guess svn branch -- try specifying it on the '
250 'command line')
251
252 return self.svn_branch
253
254 def GetTreeStatusUrl(self, error_ok=False):
255 if not self.tree_status_url:
256 error_message = ('You must configure your tree status URL by running '
257 '"git cl config".')
258 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
259 error_ok=error_ok,
260 error_message=error_message)
261 return self.tree_status_url
262
263 def GetViewVCUrl(self):
264 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000265 self.viewvc_url = gclient_utils.UpgradeToHttps(
266 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000267 return self.viewvc_url
268
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000269 def GetDefaultCCList(self):
270 return self._GetConfig('rietveld.cc', error_ok=True)
271
ukai@chromium.orge8077812012-02-03 03:41:46 +0000272 def GetIsGerrit(self):
273 """Return true if this repo is assosiated with gerrit code review system."""
274 if self.is_gerrit is None:
275 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
276 return self.is_gerrit
277
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278 def _GetConfig(self, param, **kwargs):
279 self.LazyUpdateIfNeeded()
280 return RunGit(['config', param], **kwargs).strip()
281
282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000283def ShortBranchName(branch):
284 """Convert a name like 'refs/heads/foo' to just 'foo'."""
285 return branch.replace('refs/heads/', '')
286
287
288class Changelist(object):
289 def __init__(self, branchref=None):
290 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000291 global settings
292 if not settings:
293 # Happens when git_cl.py is used as a utility library.
294 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 settings.GetDefaultServerUrl()
296 self.branchref = branchref
297 if self.branchref:
298 self.branch = ShortBranchName(self.branchref)
299 else:
300 self.branch = None
301 self.rietveld_server = None
302 self.upstream_branch = None
303 self.has_issue = False
304 self.issue = None
305 self.has_description = False
306 self.description = None
307 self.has_patchset = False
308 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000309 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000310 self.cc = None
311 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000312 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000313
314 def GetCCList(self):
315 """Return the users cc'd on this CL.
316
317 Return is a string suitable for passing to gcl with the --cc flag.
318 """
319 if self.cc is None:
320 base_cc = settings .GetDefaultCCList()
321 more_cc = ','.join(self.watchers)
322 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
323 return self.cc
324
325 def SetWatchers(self, watchers):
326 """Set the list of email addresses that should be cc'd based on the changed
327 files in this CL.
328 """
329 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000330
331 def GetBranch(self):
332 """Returns the short branch name, e.g. 'master'."""
333 if not self.branch:
334 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
335 self.branch = ShortBranchName(self.branchref)
336 return self.branch
337
338 def GetBranchRef(self):
339 """Returns the full branch name, e.g. 'refs/heads/master'."""
340 self.GetBranch() # Poke the lazy loader.
341 return self.branchref
342
343 def FetchUpstreamTuple(self):
344 """Returns a tuple containg remote and remote ref,
345 e.g. 'origin', 'refs/heads/master'
346 """
347 remote = '.'
348 branch = self.GetBranch()
349 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
350 error_ok=True).strip()
351 if upstream_branch:
352 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
353 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000354 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
355 error_ok=True).strip()
356 if upstream_branch:
357 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000358 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000359 # Fall back on trying a git-svn upstream branch.
360 if settings.GetIsGitSvn():
361 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000362 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000363 # Else, try to guess the origin remote.
364 remote_branches = RunGit(['branch', '-r']).split()
365 if 'origin/master' in remote_branches:
366 # Fall back on origin/master if it exits.
367 remote = 'origin'
368 upstream_branch = 'refs/heads/master'
369 elif 'origin/trunk' in remote_branches:
370 # Fall back on origin/trunk if it exists. Generally a shared
371 # git-svn clone
372 remote = 'origin'
373 upstream_branch = 'refs/heads/trunk'
374 else:
375 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000376Either pass complete "git diff"-style arguments, like
377 git cl upload origin/master
378or verify this branch is set up to track another (via the --track argument to
379"git checkout -b ...").""")
380
381 return remote, upstream_branch
382
383 def GetUpstreamBranch(self):
384 if self.upstream_branch is None:
385 remote, upstream_branch = self.FetchUpstreamTuple()
386 if remote is not '.':
387 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
388 self.upstream_branch = upstream_branch
389 return self.upstream_branch
390
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000391 def GetRemote(self):
392 if not self._remote:
393 self._remote = self.FetchUpstreamTuple()[0]
394 if self._remote == '.':
395
396 remotes = RunGit(['remote'], error_ok=True).split()
397 if len(remotes) == 1:
398 self._remote, = remotes
399 elif 'origin' in remotes:
400 self._remote = 'origin'
401 logging.warning('Could not determine which remote this change is '
402 'associated with, so defaulting to "%s". This may '
403 'not be what you want. You may prevent this message '
404 'by running "git svn info" as documented here: %s',
405 self._remote,
406 GIT_INSTRUCTIONS_URL)
407 else:
408 logging.warn('Could not determine which remote this change is '
409 'associated with. You may prevent this message by '
410 'running "git svn info" as documented here: %s',
411 GIT_INSTRUCTIONS_URL)
412 return self._remote
413
414
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 def GetRemoteUrl(self):
416 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
417
418 Returns None if there is no remote.
419 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000420 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000421 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
422
423 def GetIssue(self):
424 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
426 if issue:
427 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 else:
429 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 self.has_issue = True
431 return self.issue
432
433 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000434 if not self.rietveld_server:
435 # If we're on a branch then get the server potentially associated
436 # with that branch.
437 if self.GetIssue():
438 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
439 ['config', self._RietveldServer()], error_ok=True).strip())
440 if not self.rietveld_server:
441 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000442 return self.rietveld_server
443
444 def GetIssueURL(self):
445 """Get the URL for a particular issue."""
446 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
447
448 def GetDescription(self, pretty=False):
449 if not self.has_description:
450 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000451 issue = int(self.GetIssue())
452 try:
453 self.description = self.RpcServer().get_description(issue).strip()
454 except urllib2.HTTPError, e:
455 if e.code == 404:
456 DieWithError(
457 ('\nWhile fetching the description for issue %d, received a '
458 '404 (not found)\n'
459 'error. It is likely that you deleted this '
460 'issue on the server. If this is the\n'
461 'case, please run\n\n'
462 ' git cl issue 0\n\n'
463 'to clear the association with the deleted issue. Then run '
464 'this command again.') % issue)
465 else:
466 DieWithError(
467 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000468 self.has_description = True
469 if pretty:
470 wrapper = textwrap.TextWrapper()
471 wrapper.initial_indent = wrapper.subsequent_indent = ' '
472 return wrapper.fill(self.description)
473 return self.description
474
475 def GetPatchset(self):
476 if not self.has_patchset:
477 patchset = RunGit(['config', self._PatchsetSetting()],
478 error_ok=True).strip()
479 if patchset:
480 self.patchset = patchset
481 else:
482 self.patchset = None
483 self.has_patchset = True
484 return self.patchset
485
486 def SetPatchset(self, patchset):
487 """Set this branch's patchset. If patchset=0, clears the patchset."""
488 if patchset:
489 RunGit(['config', self._PatchsetSetting(), str(patchset)])
490 else:
491 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000492 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 self.has_patchset = False
494
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000495 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000496 patchset = self.RpcServer().get_issue_properties(
497 int(issue), False)['patchsets'][-1]
498 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000499 '/download/issue%s_%s.diff' % (issue, patchset))
500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000501 def SetIssue(self, issue):
502 """Set this branch's issue. If issue=0, clears the issue."""
503 if issue:
504 RunGit(['config', self._IssueSetting(), str(issue)])
505 if self.rietveld_server:
506 RunGit(['config', self._RietveldServer(), self.rietveld_server])
507 else:
508 RunGit(['config', '--unset', self._IssueSetting()])
509 self.SetPatchset(0)
510 self.has_issue = False
511
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000512 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000513 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
514 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000515
516 # We use the sha1 of HEAD as a name of this change.
517 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000518 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000519 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000520 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000521 except subprocess2.CalledProcessError:
522 DieWithError(
523 ('\nFailed to diff against upstream branch %s!\n\n'
524 'This branch probably doesn\'t exist anymore. To reset the\n'
525 'tracking branch, please run\n'
526 ' git branch --set-upstream %s trunk\n'
527 'replacing trunk with origin/master or the relevant branch') %
528 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000529
530 issue = ConvertToInteger(self.GetIssue())
531 patchset = ConvertToInteger(self.GetPatchset())
532 if issue:
533 description = self.GetDescription()
534 else:
535 # If the change was never uploaded, use the log messages of all commits
536 # up to the branch point, as git cl upload will prefill the description
537 # with these log messages.
538 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
539 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000540
541 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000542 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000543 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000544 name,
545 description,
546 absroot,
547 files,
548 issue,
549 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000550 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000551
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000552 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
553 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
554 change = self.GetChange(upstream_branch, author)
555
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000556 # Apply watchlists on upload.
557 if not committing:
558 watchlist = watchlists.Watchlists(change.RepositoryRoot())
559 files = [f.LocalPath() for f in change.AffectedFiles()]
560 self.SetWatchers(watchlist.GetWatchersForPaths(files))
561
562 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000563 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000564 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000565 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000566 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000567 except presubmit_support.PresubmitFailure, e:
568 DieWithError(
569 ('%s\nMaybe your depot_tools is out of date?\n'
570 'If all fails, contact maruel@') % e)
571
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000572 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000573 """Updates the description and closes the issue."""
574 issue = int(self.GetIssue())
575 self.RpcServer().update_description(issue, self.description)
576 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000577
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000578 def SetFlag(self, flag, value):
579 """Patchset must match."""
580 if not self.GetPatchset():
581 DieWithError('The patchset needs to match. Send another patchset.')
582 try:
583 return self.RpcServer().set_flag(
584 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
585 except urllib2.HTTPError, e:
586 if e.code == 404:
587 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
588 if e.code == 403:
589 DieWithError(
590 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
591 'match?') % (self.GetIssue(), self.GetPatchset()))
592 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000593
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000594 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000595 """Returns an upload.RpcServer() to access this review's rietveld instance.
596 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000597 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000598 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
599 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000600 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601
602 def _IssueSetting(self):
603 """Return the git setting that stores this change's issue."""
604 return 'branch.%s.rietveldissue' % self.GetBranch()
605
606 def _PatchsetSetting(self):
607 """Return the git setting that stores this change's most recent patchset."""
608 return 'branch.%s.rietveldpatchset' % self.GetBranch()
609
610 def _RietveldServer(self):
611 """Returns the git setting that stores this change's rietveld server."""
612 return 'branch.%s.rietveldserver' % self.GetBranch()
613
614
615def GetCodereviewSettingsInteractively():
616 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000617 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618 server = settings.GetDefaultServerUrl(error_ok=True)
619 prompt = 'Rietveld server (host[:port])'
620 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000621 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 if not server and not newserver:
623 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000624 if newserver:
625 newserver = gclient_utils.UpgradeToHttps(newserver)
626 if newserver != server:
627 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000629 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 prompt = caption
631 if initial:
632 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000633 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 if new_val == 'x':
635 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000636 elif new_val:
637 if is_url:
638 new_val = gclient_utils.UpgradeToHttps(new_val)
639 if new_val != initial:
640 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000642 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000644 'tree-status-url', False)
645 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646
647 # TODO: configure a default branch to diff against, rather than this
648 # svn-based hackery.
649
650
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000651class ChangeDescription(object):
652 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000653 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000654 self.log_desc = log_desc
655 self.reviewers = reviewers
656 self.description = self.log_desc
657
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000658 def Prompt(self):
659 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000660# This will displayed on the codereview site.
661# The first line will also be used as the subject of the review.
662"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000663 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000664 if ('\nR=' not in self.description and
665 '\nTBR=' not in self.description and
666 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000667 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000668 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000669 content += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000670 if '\nTEST=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000671 content += '\nTEST='
672 content = content.rstrip('\n') + '\n'
673 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000674 if not content:
675 DieWithError('Running editor failed')
676 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000677 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000678 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000679 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000680
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000681 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000682 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000683 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000684 # Retrieves all reviewer lines
685 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000686 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000687 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000688 if reviewers:
689 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000690
691 def IsEmpty(self):
692 return not self.description
693
694
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695def FindCodereviewSettingsFile(filename='codereview.settings'):
696 """Finds the given file starting in the cwd and going up.
697
698 Only looks up to the top of the repository unless an
699 'inherit-review-settings-ok' file exists in the root of the repository.
700 """
701 inherit_ok_file = 'inherit-review-settings-ok'
702 cwd = os.getcwd()
703 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
704 if os.path.isfile(os.path.join(root, inherit_ok_file)):
705 root = '/'
706 while True:
707 if filename in os.listdir(cwd):
708 if os.path.isfile(os.path.join(cwd, filename)):
709 return open(os.path.join(cwd, filename))
710 if cwd == root:
711 break
712 cwd = os.path.dirname(cwd)
713
714
715def LoadCodereviewSettingsFromFile(fileobj):
716 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000717 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 def SetProperty(name, setting, unset_error_ok=False):
720 fullname = 'rietveld.' + name
721 if setting in keyvals:
722 RunGit(['config', fullname, keyvals[setting]])
723 else:
724 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
725
726 SetProperty('server', 'CODE_REVIEW_SERVER')
727 # Only server setting is required. Other settings can be absent.
728 # In that case, we ignore errors raised during option deletion attempt.
729 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
730 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
731 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
732
ukai@chromium.orge8077812012-02-03 03:41:46 +0000733 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
734 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
735 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000736
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
738 #should be of the form
739 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
740 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
741 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
742 keyvals['ORIGIN_URL_CONFIG']])
743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000745def DownloadHooks(force):
746 """downloads hooks
747
748 Args:
749 force: True to update hooks. False to install hooks if not present.
750 """
751 if not settings.GetIsGerrit():
752 return
753 server_url = settings.GetDefaultServerUrl()
754 src = '%s/tools/hooks/commit-msg' % server_url
755 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
756 if not os.access(dst, os.X_OK):
757 if os.path.exists(dst):
758 if not force:
759 return
760 os.remove(dst)
761 try:
762 urllib.urlretrieve(src, dst)
763 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
764 except Exception:
765 if os.path.exists(dst):
766 os.remove(dst)
767 DieWithError('\nFailed to download hooks from %s' % src)
768
769
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770@usage('[repo root containing codereview.settings]')
771def CMDconfig(parser, args):
772 """edit configuration for this tree"""
773
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000774 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 if len(args) == 0:
776 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000777 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 return 0
779
780 url = args[0]
781 if not url.endswith('codereview.settings'):
782 url = os.path.join(url, 'codereview.settings')
783
784 # Load code review settings and download hooks (if available).
785 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000786 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 return 0
788
789
790def CMDstatus(parser, args):
791 """show status of changelists"""
792 parser.add_option('--field',
793 help='print only specific field (desc|id|patch|url)')
794 (options, args) = parser.parse_args(args)
795
796 # TODO: maybe make show_branches a flag if necessary.
797 show_branches = not options.field
798
799 if show_branches:
800 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
801 if branches:
802 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000803 changes = (Changelist(branchref=b) for b in branches.splitlines())
804 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
805 alignment = max(5, max(len(b) for b in branches))
806 for branch in sorted(branches):
807 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808
809 cl = Changelist()
810 if options.field:
811 if options.field.startswith('desc'):
812 print cl.GetDescription()
813 elif options.field == 'id':
814 issueid = cl.GetIssue()
815 if issueid:
816 print issueid
817 elif options.field == 'patch':
818 patchset = cl.GetPatchset()
819 if patchset:
820 print patchset
821 elif options.field == 'url':
822 url = cl.GetIssueURL()
823 if url:
824 print url
825 else:
826 print
827 print 'Current branch:',
828 if not cl.GetIssue():
829 print 'no issue assigned.'
830 return 0
831 print cl.GetBranch()
832 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
833 print 'Issue description:'
834 print cl.GetDescription(pretty=True)
835 return 0
836
837
838@usage('[issue_number]')
839def CMDissue(parser, args):
840 """Set or display the current code review issue number.
841
842 Pass issue number 0 to clear the current issue.
843"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000844 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845
846 cl = Changelist()
847 if len(args) > 0:
848 try:
849 issue = int(args[0])
850 except ValueError:
851 DieWithError('Pass a number to set the issue or none to list it.\n'
852 'Maybe you want to run git cl status?')
853 cl.SetIssue(issue)
854 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
855 return 0
856
857
858def CreateDescriptionFromLog(args):
859 """Pulls out the commit log to use as a base for the CL description."""
860 log_args = []
861 if len(args) == 1 and not args[0].endswith('.'):
862 log_args = [args[0] + '..']
863 elif len(args) == 1 and args[0].endswith('...'):
864 log_args = [args[0][:-1]]
865 elif len(args) == 2:
866 log_args = [args[0] + '..' + args[1]]
867 else:
868 log_args = args[:] # Hope for the best!
869 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
870
871
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000872def ConvertToInteger(inputval):
873 """Convert a string to integer, but returns either an int or None."""
874 try:
875 return int(inputval)
876 except (TypeError, ValueError):
877 return None
878
879
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880def CMDpresubmit(parser, args):
881 """run presubmit tests on the current changelist"""
882 parser.add_option('--upload', action='store_true',
883 help='Run upload hook instead of the push/dcommit hook')
884 (options, args) = parser.parse_args(args)
885
886 # Make sure index is up-to-date before running diff-index.
887 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
888 if RunGit(['diff-index', 'HEAD']):
889 # TODO(maruel): Is this really necessary?
890 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
891 return 1
892
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000893 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894 if args:
895 base_branch = args[0]
896 else:
897 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000898 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000900 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000901 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000902 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000903 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904
905
ukai@chromium.orge8077812012-02-03 03:41:46 +0000906def GerritUpload(options, args, cl):
907 """upload the current branch to gerrit."""
908 # We assume the remote called "origin" is the one we want.
909 # It is probably not worthwhile to support different workflows.
910 remote = 'origin'
911 branch = 'master'
912 if options.target_branch:
913 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000915 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000916 if options.reviewers:
917 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000918 change_desc = ChangeDescription(log_desc, options.reviewers)
919 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000920 if change_desc.IsEmpty():
921 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 return 1
923
ukai@chromium.orge8077812012-02-03 03:41:46 +0000924 receive_options = []
925 cc = cl.GetCCList().split(',')
926 if options.cc:
927 cc += options.cc.split(',')
928 cc = filter(None, cc)
929 if cc:
930 receive_options += ['--cc=' + email for email in cc]
931 if change_desc.reviewers:
932 reviewers = filter(None, change_desc.reviewers.split(','))
933 if reviewers:
934 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935
ukai@chromium.orge8077812012-02-03 03:41:46 +0000936 git_command = ['push']
937 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000938 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000939 ' '.join(receive_options))
940 git_command += [remote, 'HEAD:refs/for/' + branch]
941 RunGit(git_command)
942 # TODO(ukai): parse Change-Id: and set issue number?
943 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000944
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945
ukai@chromium.orge8077812012-02-03 03:41:46 +0000946def RietveldUpload(options, args, cl):
947 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948 upload_args = ['--assume_yes'] # Don't ask about untracked files.
949 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 if options.emulate_svn_auto_props:
951 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952
953 change_desc = None
954
955 if cl.GetIssue():
956 if options.message:
957 upload_args.extend(['--message', options.message])
958 upload_args.extend(['--issue', cl.GetIssue()])
959 print ("This branch is associated with issue %s. "
960 "Adding patch to that issue." % cl.GetIssue())
961 else:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000962 message = options.message or CreateDescriptionFromLog(args)
963 change_desc = ChangeDescription(message, options.reviewers)
964 if not options.force:
965 change_desc.Prompt()
966 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000967
968 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 print "Description is empty; aborting."
970 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000971
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000972 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000973 if change_desc.reviewers:
974 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000975 if options.send_mail:
976 if not change_desc.reviewers:
977 DieWithError("Must specify reviewers to send email.")
978 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000979 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000980 if cc:
981 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982
983 # Include the upstream repo's URL in the change -- this is useful for
984 # projects that have their source spread across multiple repos.
985 remote_url = None
986 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000987 # URL is dependent on the current directory.
988 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989 if data:
990 keys = dict(line.split(': ', 1) for line in data.splitlines()
991 if ': ' in line)
992 remote_url = keys.get('URL', None)
993 else:
994 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
995 remote_url = (cl.GetRemoteUrl() + '@'
996 + cl.GetUpstreamBranch().split('/')[-1])
997 if remote_url:
998 upload_args.extend(['--base_url', remote_url])
999
1000 try:
1001 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001002 except KeyboardInterrupt:
1003 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004 except:
1005 # If we got an exception after the user typed a description for their
1006 # change, back up the description before re-raising.
1007 if change_desc:
1008 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1009 print '\nGot exception while uploading -- saving description to %s\n' \
1010 % backup_path
1011 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001012 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 backup_file.close()
1014 raise
1015
1016 if not cl.GetIssue():
1017 cl.SetIssue(issue)
1018 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001019
1020 if options.use_commit_queue:
1021 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022 return 0
1023
1024
ukai@chromium.orge8077812012-02-03 03:41:46 +00001025@usage('[args to "git diff"]')
1026def CMDupload(parser, args):
1027 """upload the current changelist to codereview"""
1028 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1029 help='bypass upload presubmit hook')
1030 parser.add_option('-f', action='store_true', dest='force',
1031 help="force yes to questions (don't prompt)")
1032 parser.add_option('-m', dest='message', help='message for patch')
1033 parser.add_option('-r', '--reviewers',
1034 help='reviewer email addresses')
1035 parser.add_option('--cc',
1036 help='cc email addresses')
1037 parser.add_option('--send-mail', action='store_true',
1038 help='send email to reviewer immediately')
1039 parser.add_option("--emulate_svn_auto_props", action="store_true",
1040 dest="emulate_svn_auto_props",
1041 help="Emulate Subversion's auto properties feature.")
1042 parser.add_option("--desc_from_logs", action="store_true",
1043 dest="from_logs",
1044 help="""Squashes git commit logs into change description and
1045 uses message as subject""")
1046 parser.add_option('-c', '--use-commit-queue', action='store_true',
1047 help='tell the commit queue to commit this patchset')
1048 if settings.GetIsGerrit():
1049 parser.add_option('--target_branch', dest='target_branch', default='master',
1050 help='target branch to upload')
1051 (options, args) = parser.parse_args(args)
1052
1053 # Make sure index is up-to-date before running diff-index.
1054 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1055 if RunGit(['diff-index', 'HEAD']):
1056 print 'Cannot upload with a dirty tree. You must commit locally first.'
1057 return 1
1058
1059 cl = Changelist()
1060 if args:
1061 # TODO(ukai): is it ok for gerrit case?
1062 base_branch = args[0]
1063 else:
1064 # Default to diffing against the "upstream" branch.
1065 base_branch = cl.GetUpstreamBranch()
1066 args = [base_branch + "..."]
1067
1068 if not options.bypass_hooks:
1069 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1070 may_prompt=not options.force,
1071 verbose=options.verbose,
1072 author=None)
1073 if not hook_results.should_continue():
1074 return 1
1075 if not options.reviewers and hook_results.reviewers:
1076 options.reviewers = hook_results.reviewers
1077
1078 # --no-ext-diff is broken in some versions of Git, so try to work around
1079 # this by overriding the environment (but there is still a problem if the
1080 # git config key "diff.external" is used).
1081 env = os.environ.copy()
1082 if 'GIT_EXTERNAL_DIFF' in env:
1083 del env['GIT_EXTERNAL_DIFF']
1084 subprocess2.call(
1085 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1086
1087 if settings.GetIsGerrit():
1088 return GerritUpload(options, args, cl)
1089 return RietveldUpload(options, args, cl)
1090
1091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092def SendUpstream(parser, args, cmd):
1093 """Common code for CmdPush and CmdDCommit
1094
1095 Squashed commit into a single.
1096 Updates changelog with metadata (e.g. pointer to review).
1097 Pushes/dcommits the code upstream.
1098 Updates review and closes.
1099 """
1100 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1101 help='bypass upload presubmit hook')
1102 parser.add_option('-m', dest='message',
1103 help="override review description")
1104 parser.add_option('-f', action='store_true', dest='force',
1105 help="force yes to questions (don't prompt)")
1106 parser.add_option('-c', dest='contributor',
1107 help="external contributor for patch (appended to " +
1108 "description and used as author for git). Should be " +
1109 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 (options, args) = parser.parse_args(args)
1111 cl = Changelist()
1112
1113 if not args or cmd == 'push':
1114 # Default to merging against our best guess of the upstream branch.
1115 args = [cl.GetUpstreamBranch()]
1116
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001117 if options.contributor:
1118 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1119 print "Please provide contibutor as 'First Last <email@example.com>'"
1120 return 1
1121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 base_branch = args[0]
1123
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001124 # Make sure index is up-to-date before running diff-index.
1125 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 if RunGit(['diff-index', 'HEAD']):
1127 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1128 return 1
1129
1130 # This rev-list syntax means "show all commits not in my branch that
1131 # are in base_branch".
1132 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1133 base_branch]).splitlines()
1134 if upstream_commits:
1135 print ('Base branch "%s" has %d commits '
1136 'not in this branch.' % (base_branch, len(upstream_commits)))
1137 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1138 return 1
1139
1140 if cmd == 'dcommit':
1141 # This is the revision `svn dcommit` will commit on top of.
1142 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1143 '--pretty=format:%H'])
1144 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1145 if extra_commits:
1146 print ('This branch has %d additional commits not upstreamed yet.'
1147 % len(extra_commits.splitlines()))
1148 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1149 'before attempting to %s.' % (base_branch, cmd))
1150 return 1
1151
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001152 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001153 author = None
1154 if options.contributor:
1155 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001156 hook_results = cl.RunHook(
1157 committing=True,
1158 upstream_branch=base_branch,
1159 may_prompt=not options.force,
1160 verbose=options.verbose,
1161 author=author)
1162 if not hook_results.should_continue():
1163 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164
1165 if cmd == 'dcommit':
1166 # Check the tree status if the tree status URL is set.
1167 status = GetTreeStatus()
1168 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001169 print('The tree is closed. Please wait for it to reopen. Use '
1170 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 return 1
1172 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001173 print('Unable to determine tree status. Please verify manually and '
1174 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001175 else:
1176 breakpad.SendStack(
1177 'GitClHooksBypassedCommit',
1178 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001179 (cl.GetRietveldServer(), cl.GetIssue()),
1180 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181
1182 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001183 if not description and cl.GetIssue():
1184 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001186 if not description:
1187 print 'No description set.'
1188 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1189 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001191 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193
1194 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 description += "\nPatch from %s." % options.contributor
1196 print 'Description:', repr(description)
1197
1198 branches = [base_branch, cl.GetBranchRef()]
1199 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001200 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001201 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202
1203 # We want to squash all this branch's commits into one commit with the
1204 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001205 # We do this by doing a "reset --soft" to the base branch (which keeps
1206 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 MERGE_BRANCH = 'git-cl-commit'
1208 # Delete the merge branch if it already exists.
1209 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1210 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1211 RunGit(['branch', '-D', MERGE_BRANCH])
1212
1213 # We might be in a directory that's present in this branch but not in the
1214 # trunk. Move up to the top of the tree so that git commands that expect a
1215 # valid CWD won't fail after we check out the merge branch.
1216 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1217 if rel_base_path:
1218 os.chdir(rel_base_path)
1219
1220 # Stuff our change into the merge branch.
1221 # We wrap in a try...finally block so if anything goes wrong,
1222 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001223 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001225 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1226 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if options.contributor:
1228 RunGit(['commit', '--author', options.contributor, '-m', description])
1229 else:
1230 RunGit(['commit', '-m', description])
1231 if cmd == 'push':
1232 # push the merge branch.
1233 remote, branch = cl.FetchUpstreamTuple()
1234 retcode, output = RunGitWithCode(
1235 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1236 logging.debug(output)
1237 else:
1238 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001239 retcode, output = RunGitWithCode(['svn', 'dcommit',
1240 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 finally:
1242 # And then swap back to the original branch and clean up.
1243 RunGit(['checkout', '-q', cl.GetBranch()])
1244 RunGit(['branch', '-D', MERGE_BRANCH])
1245
1246 if cl.GetIssue():
1247 if cmd == 'dcommit' and 'Committed r' in output:
1248 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1249 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001250 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1251 for l in output.splitlines(False))
1252 match = filter(None, match)
1253 if len(match) != 1:
1254 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1255 output)
1256 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
1258 return 1
1259 viewvc_url = settings.GetViewVCUrl()
1260 if viewvc_url and revision:
1261 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1262 print ('Closing issue '
1263 '(you may be prompted for your codereview password)...')
1264 cl.CloseIssue()
1265 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001266
1267 if retcode == 0:
1268 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1269 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001270 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 return 0
1273
1274
1275@usage('[upstream branch to apply against]')
1276def CMDdcommit(parser, args):
1277 """commit the current changelist via git-svn"""
1278 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001279 message = """This doesn't appear to be an SVN repository.
1280If your project has a git mirror with an upstream SVN master, you probably need
1281to run 'git svn init', see your project's git mirror documentation.
1282If your project has a true writeable upstream repository, you probably want
1283to run 'git cl push' instead.
1284Choose wisely, if you get this wrong, your commit might appear to succeed but
1285will instead be silently ignored."""
1286 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001287 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 return SendUpstream(parser, args, 'dcommit')
1289
1290
1291@usage('[upstream branch to apply against]')
1292def CMDpush(parser, args):
1293 """commit the current changelist via git"""
1294 if settings.GetIsGitSvn():
1295 print('This appears to be an SVN repository.')
1296 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001297 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 return SendUpstream(parser, args, 'push')
1299
1300
1301@usage('<patch url or issue id>')
1302def CMDpatch(parser, args):
1303 """patch in a code review"""
1304 parser.add_option('-b', dest='newbranch',
1305 help='create a new branch off trunk for the patch')
1306 parser.add_option('-f', action='store_true', dest='force',
1307 help='with -b, clobber any existing branch')
1308 parser.add_option('--reject', action='store_true', dest='reject',
1309 help='allow failed patches and spew .rej files')
1310 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1311 help="don't commit after patch applies")
1312 (options, args) = parser.parse_args(args)
1313 if len(args) != 1:
1314 parser.print_help()
1315 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001316 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001318 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001319 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001320
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001321 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001323 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001324 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001326 # Assume it's a URL to the patch. Default to https.
1327 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001328 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001329 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 DieWithError('Must pass an issue ID or full URL for '
1331 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001332 issue = match.group(1)
1333 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334
1335 if options.newbranch:
1336 if options.force:
1337 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001338 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 RunGit(['checkout', '-b', options.newbranch,
1340 Changelist().GetUpstreamBranch()])
1341
1342 # Switch up to the top-level directory, if necessary, in preparation for
1343 # applying the patch.
1344 top = RunGit(['rev-parse', '--show-cdup']).strip()
1345 if top:
1346 os.chdir(top)
1347
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348 # Git patches have a/ at the beginning of source paths. We strip that out
1349 # with a sed script rather than the -p flag to patch so we can feed either
1350 # Git or svn-style patches into the same apply command.
1351 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001352 try:
1353 patch_data = subprocess2.check_output(
1354 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1355 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 DieWithError('Git patch mungling failed.')
1357 logging.info(patch_data)
1358 # We use "git apply" to apply the patch instead of "patch" so that we can
1359 # pick up file adds.
1360 # The --index flag means: also insert into the index (so we catch adds).
1361 cmd = ['git', 'apply', '--index', '-p0']
1362 if options.reject:
1363 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001364 try:
1365 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1366 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 DieWithError('Failed to apply the patch')
1368
1369 # If we had an issue, commit the current state and register the issue.
1370 if not options.nocommit:
1371 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1372 cl = Changelist()
1373 cl.SetIssue(issue)
1374 print "Committed patch."
1375 else:
1376 print "Patch applied to index."
1377 return 0
1378
1379
1380def CMDrebase(parser, args):
1381 """rebase current branch on top of svn repo"""
1382 # Provide a wrapper for git svn rebase to help avoid accidental
1383 # git svn dcommit.
1384 # It's the only command that doesn't use parser at all since we just defer
1385 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001386 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387
1388
1389def GetTreeStatus():
1390 """Fetches the tree status and returns either 'open', 'closed',
1391 'unknown' or 'unset'."""
1392 url = settings.GetTreeStatusUrl(error_ok=True)
1393 if url:
1394 status = urllib2.urlopen(url).read().lower()
1395 if status.find('closed') != -1 or status == '0':
1396 return 'closed'
1397 elif status.find('open') != -1 or status == '1':
1398 return 'open'
1399 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400 return 'unset'
1401
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403def GetTreeStatusReason():
1404 """Fetches the tree status from a json url and returns the message
1405 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001406 url = settings.GetTreeStatusUrl()
1407 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 connection = urllib2.urlopen(json_url)
1409 status = json.loads(connection.read())
1410 connection.close()
1411 return status['message']
1412
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001413
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414def CMDtree(parser, args):
1415 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001416 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 status = GetTreeStatus()
1418 if 'unset' == status:
1419 print 'You must configure your tree status URL by running "git cl config".'
1420 return 2
1421
1422 print "The tree is %s" % status
1423 print
1424 print GetTreeStatusReason()
1425 if status != 'open':
1426 return 1
1427 return 0
1428
1429
1430def CMDupstream(parser, args):
1431 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001432 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001433 if args:
1434 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 cl = Changelist()
1436 print cl.GetUpstreamBranch()
1437 return 0
1438
1439
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001440def CMDset_commit(parser, args):
1441 """set the commit bit"""
1442 _, args = parser.parse_args(args)
1443 if args:
1444 parser.error('Unrecognized args: %s' % ' '.join(args))
1445 cl = Changelist()
1446 cl.SetFlag('commit', '1')
1447 return 0
1448
1449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450def Command(name):
1451 return getattr(sys.modules[__name__], 'CMD' + name, None)
1452
1453
1454def CMDhelp(parser, args):
1455 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001456 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 if len(args) == 1:
1458 return main(args + ['--help'])
1459 parser.print_help()
1460 return 0
1461
1462
1463def GenUsage(parser, command):
1464 """Modify an OptParse object with the function's documentation."""
1465 obj = Command(command)
1466 more = getattr(obj, 'usage_more', '')
1467 if command == 'help':
1468 command = '<command>'
1469 else:
1470 # OptParser.description prefer nicely non-formatted strings.
1471 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1472 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1473
1474
1475def main(argv):
1476 """Doesn't parse the arguments here, just find the right subcommand to
1477 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001478 if sys.hexversion < 0x02060000:
1479 print >> sys.stderr, (
1480 '\nYour python version %s is unsupported, please upgrade.\n' %
1481 sys.version.split(' ', 1)[0])
1482 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001483 # Reload settings.
1484 global settings
1485 settings = Settings()
1486
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 # Do it late so all commands are listed.
1488 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1489 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1490 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1491
1492 # Create the option parse and add --verbose support.
1493 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001494 parser.add_option(
1495 '-v', '--verbose', action='count', default=0,
1496 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497 old_parser_args = parser.parse_args
1498 def Parse(args):
1499 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001500 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001502 elif options.verbose:
1503 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 else:
1505 logging.basicConfig(level=logging.WARNING)
1506 return options, args
1507 parser.parse_args = Parse
1508
1509 if argv:
1510 command = Command(argv[0])
1511 if command:
1512 # "fix" the usage and the description now that we know the subcommand.
1513 GenUsage(parser, argv[0])
1514 try:
1515 return command(parser, argv[1:])
1516 except urllib2.HTTPError, e:
1517 if e.code != 500:
1518 raise
1519 DieWithError(
1520 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1521 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1522
1523 # Not a known command. Default to help.
1524 GenUsage(parser, 'help')
1525 return CMDhelp(parser, argv)
1526
1527
1528if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001529 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530 sys.exit(main(sys.argv[1:]))