blob: e632d08aad81aa3918608871355de2d33035b082 [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:'
803 for branch in sorted(branches.splitlines()):
804 cl = Changelist(branchref=branch)
805 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
806
807 cl = Changelist()
808 if options.field:
809 if options.field.startswith('desc'):
810 print cl.GetDescription()
811 elif options.field == 'id':
812 issueid = cl.GetIssue()
813 if issueid:
814 print issueid
815 elif options.field == 'patch':
816 patchset = cl.GetPatchset()
817 if patchset:
818 print patchset
819 elif options.field == 'url':
820 url = cl.GetIssueURL()
821 if url:
822 print url
823 else:
824 print
825 print 'Current branch:',
826 if not cl.GetIssue():
827 print 'no issue assigned.'
828 return 0
829 print cl.GetBranch()
830 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
831 print 'Issue description:'
832 print cl.GetDescription(pretty=True)
833 return 0
834
835
836@usage('[issue_number]')
837def CMDissue(parser, args):
838 """Set or display the current code review issue number.
839
840 Pass issue number 0 to clear the current issue.
841"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000842 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843
844 cl = Changelist()
845 if len(args) > 0:
846 try:
847 issue = int(args[0])
848 except ValueError:
849 DieWithError('Pass a number to set the issue or none to list it.\n'
850 'Maybe you want to run git cl status?')
851 cl.SetIssue(issue)
852 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
853 return 0
854
855
856def CreateDescriptionFromLog(args):
857 """Pulls out the commit log to use as a base for the CL description."""
858 log_args = []
859 if len(args) == 1 and not args[0].endswith('.'):
860 log_args = [args[0] + '..']
861 elif len(args) == 1 and args[0].endswith('...'):
862 log_args = [args[0][:-1]]
863 elif len(args) == 2:
864 log_args = [args[0] + '..' + args[1]]
865 else:
866 log_args = args[:] # Hope for the best!
867 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
868
869
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000870def ConvertToInteger(inputval):
871 """Convert a string to integer, but returns either an int or None."""
872 try:
873 return int(inputval)
874 except (TypeError, ValueError):
875 return None
876
877
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878def CMDpresubmit(parser, args):
879 """run presubmit tests on the current changelist"""
880 parser.add_option('--upload', action='store_true',
881 help='Run upload hook instead of the push/dcommit hook')
882 (options, args) = parser.parse_args(args)
883
884 # Make sure index is up-to-date before running diff-index.
885 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
886 if RunGit(['diff-index', 'HEAD']):
887 # TODO(maruel): Is this really necessary?
888 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
889 return 1
890
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000891 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 if args:
893 base_branch = args[0]
894 else:
895 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000896 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000898 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000899 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000900 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000901 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902
903
ukai@chromium.orge8077812012-02-03 03:41:46 +0000904def GerritUpload(options, args, cl):
905 """upload the current branch to gerrit."""
906 # We assume the remote called "origin" is the one we want.
907 # It is probably not worthwhile to support different workflows.
908 remote = 'origin'
909 branch = 'master'
910 if options.target_branch:
911 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000913 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000914 if options.reviewers:
915 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000916 change_desc = ChangeDescription(log_desc, options.reviewers)
917 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000918 if change_desc.IsEmpty():
919 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000920 return 1
921
ukai@chromium.orge8077812012-02-03 03:41:46 +0000922 receive_options = []
923 cc = cl.GetCCList().split(',')
924 if options.cc:
925 cc += options.cc.split(',')
926 cc = filter(None, cc)
927 if cc:
928 receive_options += ['--cc=' + email for email in cc]
929 if change_desc.reviewers:
930 reviewers = filter(None, change_desc.reviewers.split(','))
931 if reviewers:
932 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933
ukai@chromium.orge8077812012-02-03 03:41:46 +0000934 git_command = ['push']
935 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000936 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000937 ' '.join(receive_options))
938 git_command += [remote, 'HEAD:refs/for/' + branch]
939 RunGit(git_command)
940 # TODO(ukai): parse Change-Id: and set issue number?
941 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943
ukai@chromium.orge8077812012-02-03 03:41:46 +0000944def RietveldUpload(options, args, cl):
945 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 upload_args = ['--assume_yes'] # Don't ask about untracked files.
947 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948 if options.emulate_svn_auto_props:
949 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950
951 change_desc = None
952
953 if cl.GetIssue():
954 if options.message:
955 upload_args.extend(['--message', options.message])
956 upload_args.extend(['--issue', cl.GetIssue()])
957 print ("This branch is associated with issue %s. "
958 "Adding patch to that issue." % cl.GetIssue())
959 else:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000960 message = options.message or CreateDescriptionFromLog(args)
961 change_desc = ChangeDescription(message, options.reviewers)
962 if not options.force:
963 change_desc.Prompt()
964 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000965
966 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967 print "Description is empty; aborting."
968 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000969
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000970 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000971 if change_desc.reviewers:
972 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000973 if options.send_mail:
974 if not change_desc.reviewers:
975 DieWithError("Must specify reviewers to send email.")
976 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000977 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000978 if cc:
979 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980
981 # Include the upstream repo's URL in the change -- this is useful for
982 # projects that have their source spread across multiple repos.
983 remote_url = None
984 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000985 # URL is dependent on the current directory.
986 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 if data:
988 keys = dict(line.split(': ', 1) for line in data.splitlines()
989 if ': ' in line)
990 remote_url = keys.get('URL', None)
991 else:
992 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
993 remote_url = (cl.GetRemoteUrl() + '@'
994 + cl.GetUpstreamBranch().split('/')[-1])
995 if remote_url:
996 upload_args.extend(['--base_url', remote_url])
997
998 try:
999 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001000 except KeyboardInterrupt:
1001 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 except:
1003 # If we got an exception after the user typed a description for their
1004 # change, back up the description before re-raising.
1005 if change_desc:
1006 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1007 print '\nGot exception while uploading -- saving description to %s\n' \
1008 % backup_path
1009 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001010 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 backup_file.close()
1012 raise
1013
1014 if not cl.GetIssue():
1015 cl.SetIssue(issue)
1016 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001017
1018 if options.use_commit_queue:
1019 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 return 0
1021
1022
ukai@chromium.orge8077812012-02-03 03:41:46 +00001023@usage('[args to "git diff"]')
1024def CMDupload(parser, args):
1025 """upload the current changelist to codereview"""
1026 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1027 help='bypass upload presubmit hook')
1028 parser.add_option('-f', action='store_true', dest='force',
1029 help="force yes to questions (don't prompt)")
1030 parser.add_option('-m', dest='message', help='message for patch')
1031 parser.add_option('-r', '--reviewers',
1032 help='reviewer email addresses')
1033 parser.add_option('--cc',
1034 help='cc email addresses')
1035 parser.add_option('--send-mail', action='store_true',
1036 help='send email to reviewer immediately')
1037 parser.add_option("--emulate_svn_auto_props", action="store_true",
1038 dest="emulate_svn_auto_props",
1039 help="Emulate Subversion's auto properties feature.")
1040 parser.add_option("--desc_from_logs", action="store_true",
1041 dest="from_logs",
1042 help="""Squashes git commit logs into change description and
1043 uses message as subject""")
1044 parser.add_option('-c', '--use-commit-queue', action='store_true',
1045 help='tell the commit queue to commit this patchset')
1046 if settings.GetIsGerrit():
1047 parser.add_option('--target_branch', dest='target_branch', default='master',
1048 help='target branch to upload')
1049 (options, args) = parser.parse_args(args)
1050
1051 # Make sure index is up-to-date before running diff-index.
1052 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1053 if RunGit(['diff-index', 'HEAD']):
1054 print 'Cannot upload with a dirty tree. You must commit locally first.'
1055 return 1
1056
1057 cl = Changelist()
1058 if args:
1059 # TODO(ukai): is it ok for gerrit case?
1060 base_branch = args[0]
1061 else:
1062 # Default to diffing against the "upstream" branch.
1063 base_branch = cl.GetUpstreamBranch()
1064 args = [base_branch + "..."]
1065
1066 if not options.bypass_hooks:
1067 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1068 may_prompt=not options.force,
1069 verbose=options.verbose,
1070 author=None)
1071 if not hook_results.should_continue():
1072 return 1
1073 if not options.reviewers and hook_results.reviewers:
1074 options.reviewers = hook_results.reviewers
1075
1076 # --no-ext-diff is broken in some versions of Git, so try to work around
1077 # this by overriding the environment (but there is still a problem if the
1078 # git config key "diff.external" is used).
1079 env = os.environ.copy()
1080 if 'GIT_EXTERNAL_DIFF' in env:
1081 del env['GIT_EXTERNAL_DIFF']
1082 subprocess2.call(
1083 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1084
1085 if settings.GetIsGerrit():
1086 return GerritUpload(options, args, cl)
1087 return RietveldUpload(options, args, cl)
1088
1089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090def SendUpstream(parser, args, cmd):
1091 """Common code for CmdPush and CmdDCommit
1092
1093 Squashed commit into a single.
1094 Updates changelog with metadata (e.g. pointer to review).
1095 Pushes/dcommits the code upstream.
1096 Updates review and closes.
1097 """
1098 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1099 help='bypass upload presubmit hook')
1100 parser.add_option('-m', dest='message',
1101 help="override review description")
1102 parser.add_option('-f', action='store_true', dest='force',
1103 help="force yes to questions (don't prompt)")
1104 parser.add_option('-c', dest='contributor',
1105 help="external contributor for patch (appended to " +
1106 "description and used as author for git). Should be " +
1107 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 (options, args) = parser.parse_args(args)
1109 cl = Changelist()
1110
1111 if not args or cmd == 'push':
1112 # Default to merging against our best guess of the upstream branch.
1113 args = [cl.GetUpstreamBranch()]
1114
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001115 if options.contributor:
1116 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1117 print "Please provide contibutor as 'First Last <email@example.com>'"
1118 return 1
1119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 base_branch = args[0]
1121
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001122 # Make sure index is up-to-date before running diff-index.
1123 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124 if RunGit(['diff-index', 'HEAD']):
1125 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1126 return 1
1127
1128 # This rev-list syntax means "show all commits not in my branch that
1129 # are in base_branch".
1130 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1131 base_branch]).splitlines()
1132 if upstream_commits:
1133 print ('Base branch "%s" has %d commits '
1134 'not in this branch.' % (base_branch, len(upstream_commits)))
1135 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1136 return 1
1137
1138 if cmd == 'dcommit':
1139 # This is the revision `svn dcommit` will commit on top of.
1140 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1141 '--pretty=format:%H'])
1142 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1143 if extra_commits:
1144 print ('This branch has %d additional commits not upstreamed yet.'
1145 % len(extra_commits.splitlines()))
1146 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1147 'before attempting to %s.' % (base_branch, cmd))
1148 return 1
1149
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001150 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001151 author = None
1152 if options.contributor:
1153 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001154 hook_results = cl.RunHook(
1155 committing=True,
1156 upstream_branch=base_branch,
1157 may_prompt=not options.force,
1158 verbose=options.verbose,
1159 author=author)
1160 if not hook_results.should_continue():
1161 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162
1163 if cmd == 'dcommit':
1164 # Check the tree status if the tree status URL is set.
1165 status = GetTreeStatus()
1166 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001167 print('The tree is closed. Please wait for it to reopen. Use '
1168 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 return 1
1170 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001171 print('Unable to determine tree status. Please verify manually and '
1172 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001173 else:
1174 breakpad.SendStack(
1175 'GitClHooksBypassedCommit',
1176 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001177 (cl.GetRietveldServer(), cl.GetIssue()),
1178 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179
1180 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001181 if not description and cl.GetIssue():
1182 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001184 if not description:
1185 print 'No description set.'
1186 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1187 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001189 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
1192 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 description += "\nPatch from %s." % options.contributor
1194 print 'Description:', repr(description)
1195
1196 branches = [base_branch, cl.GetBranchRef()]
1197 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001198 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001199 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200
1201 # We want to squash all this branch's commits into one commit with the
1202 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001203 # We do this by doing a "reset --soft" to the base branch (which keeps
1204 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 MERGE_BRANCH = 'git-cl-commit'
1206 # Delete the merge branch if it already exists.
1207 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1208 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1209 RunGit(['branch', '-D', MERGE_BRANCH])
1210
1211 # We might be in a directory that's present in this branch but not in the
1212 # trunk. Move up to the top of the tree so that git commands that expect a
1213 # valid CWD won't fail after we check out the merge branch.
1214 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1215 if rel_base_path:
1216 os.chdir(rel_base_path)
1217
1218 # Stuff our change into the merge branch.
1219 # We wrap in a try...finally block so if anything goes wrong,
1220 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001221 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001223 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1224 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 if options.contributor:
1226 RunGit(['commit', '--author', options.contributor, '-m', description])
1227 else:
1228 RunGit(['commit', '-m', description])
1229 if cmd == 'push':
1230 # push the merge branch.
1231 remote, branch = cl.FetchUpstreamTuple()
1232 retcode, output = RunGitWithCode(
1233 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1234 logging.debug(output)
1235 else:
1236 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001237 retcode, output = RunGitWithCode(['svn', 'dcommit',
1238 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 finally:
1240 # And then swap back to the original branch and clean up.
1241 RunGit(['checkout', '-q', cl.GetBranch()])
1242 RunGit(['branch', '-D', MERGE_BRANCH])
1243
1244 if cl.GetIssue():
1245 if cmd == 'dcommit' and 'Committed r' in output:
1246 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1247 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001248 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1249 for l in output.splitlines(False))
1250 match = filter(None, match)
1251 if len(match) != 1:
1252 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1253 output)
1254 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 else:
1256 return 1
1257 viewvc_url = settings.GetViewVCUrl()
1258 if viewvc_url and revision:
1259 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1260 print ('Closing issue '
1261 '(you may be prompted for your codereview password)...')
1262 cl.CloseIssue()
1263 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001264
1265 if retcode == 0:
1266 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1267 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001268 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 return 0
1271
1272
1273@usage('[upstream branch to apply against]')
1274def CMDdcommit(parser, args):
1275 """commit the current changelist via git-svn"""
1276 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001277 message = """This doesn't appear to be an SVN repository.
1278If your project has a git mirror with an upstream SVN master, you probably need
1279to run 'git svn init', see your project's git mirror documentation.
1280If your project has a true writeable upstream repository, you probably want
1281to run 'git cl push' instead.
1282Choose wisely, if you get this wrong, your commit might appear to succeed but
1283will instead be silently ignored."""
1284 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001285 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 return SendUpstream(parser, args, 'dcommit')
1287
1288
1289@usage('[upstream branch to apply against]')
1290def CMDpush(parser, args):
1291 """commit the current changelist via git"""
1292 if settings.GetIsGitSvn():
1293 print('This appears to be an SVN repository.')
1294 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001295 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 return SendUpstream(parser, args, 'push')
1297
1298
1299@usage('<patch url or issue id>')
1300def CMDpatch(parser, args):
1301 """patch in a code review"""
1302 parser.add_option('-b', dest='newbranch',
1303 help='create a new branch off trunk for the patch')
1304 parser.add_option('-f', action='store_true', dest='force',
1305 help='with -b, clobber any existing branch')
1306 parser.add_option('--reject', action='store_true', dest='reject',
1307 help='allow failed patches and spew .rej files')
1308 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1309 help="don't commit after patch applies")
1310 (options, args) = parser.parse_args(args)
1311 if len(args) != 1:
1312 parser.print_help()
1313 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001314 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001316 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001317 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001318
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001319 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001321 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001322 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001324 # Assume it's a URL to the patch. Default to https.
1325 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001326 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001327 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 DieWithError('Must pass an issue ID or full URL for '
1329 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001330 issue = match.group(1)
1331 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
1333 if options.newbranch:
1334 if options.force:
1335 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001336 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 RunGit(['checkout', '-b', options.newbranch,
1338 Changelist().GetUpstreamBranch()])
1339
1340 # Switch up to the top-level directory, if necessary, in preparation for
1341 # applying the patch.
1342 top = RunGit(['rev-parse', '--show-cdup']).strip()
1343 if top:
1344 os.chdir(top)
1345
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346 # Git patches have a/ at the beginning of source paths. We strip that out
1347 # with a sed script rather than the -p flag to patch so we can feed either
1348 # Git or svn-style patches into the same apply command.
1349 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001350 try:
1351 patch_data = subprocess2.check_output(
1352 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1353 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 DieWithError('Git patch mungling failed.')
1355 logging.info(patch_data)
1356 # We use "git apply" to apply the patch instead of "patch" so that we can
1357 # pick up file adds.
1358 # The --index flag means: also insert into the index (so we catch adds).
1359 cmd = ['git', 'apply', '--index', '-p0']
1360 if options.reject:
1361 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001362 try:
1363 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1364 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 DieWithError('Failed to apply the patch')
1366
1367 # If we had an issue, commit the current state and register the issue.
1368 if not options.nocommit:
1369 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1370 cl = Changelist()
1371 cl.SetIssue(issue)
1372 print "Committed patch."
1373 else:
1374 print "Patch applied to index."
1375 return 0
1376
1377
1378def CMDrebase(parser, args):
1379 """rebase current branch on top of svn repo"""
1380 # Provide a wrapper for git svn rebase to help avoid accidental
1381 # git svn dcommit.
1382 # It's the only command that doesn't use parser at all since we just defer
1383 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001384 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385
1386
1387def GetTreeStatus():
1388 """Fetches the tree status and returns either 'open', 'closed',
1389 'unknown' or 'unset'."""
1390 url = settings.GetTreeStatusUrl(error_ok=True)
1391 if url:
1392 status = urllib2.urlopen(url).read().lower()
1393 if status.find('closed') != -1 or status == '0':
1394 return 'closed'
1395 elif status.find('open') != -1 or status == '1':
1396 return 'open'
1397 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 return 'unset'
1399
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401def GetTreeStatusReason():
1402 """Fetches the tree status from a json url and returns the message
1403 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001404 url = settings.GetTreeStatusUrl()
1405 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406 connection = urllib2.urlopen(json_url)
1407 status = json.loads(connection.read())
1408 connection.close()
1409 return status['message']
1410
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001411
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412def CMDtree(parser, args):
1413 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001414 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 status = GetTreeStatus()
1416 if 'unset' == status:
1417 print 'You must configure your tree status URL by running "git cl config".'
1418 return 2
1419
1420 print "The tree is %s" % status
1421 print
1422 print GetTreeStatusReason()
1423 if status != 'open':
1424 return 1
1425 return 0
1426
1427
1428def CMDupstream(parser, args):
1429 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001430 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001431 if args:
1432 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 cl = Changelist()
1434 print cl.GetUpstreamBranch()
1435 return 0
1436
1437
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001438def CMDset_commit(parser, args):
1439 """set the commit bit"""
1440 _, args = parser.parse_args(args)
1441 if args:
1442 parser.error('Unrecognized args: %s' % ' '.join(args))
1443 cl = Changelist()
1444 cl.SetFlag('commit', '1')
1445 return 0
1446
1447
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448def Command(name):
1449 return getattr(sys.modules[__name__], 'CMD' + name, None)
1450
1451
1452def CMDhelp(parser, args):
1453 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001454 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455 if len(args) == 1:
1456 return main(args + ['--help'])
1457 parser.print_help()
1458 return 0
1459
1460
1461def GenUsage(parser, command):
1462 """Modify an OptParse object with the function's documentation."""
1463 obj = Command(command)
1464 more = getattr(obj, 'usage_more', '')
1465 if command == 'help':
1466 command = '<command>'
1467 else:
1468 # OptParser.description prefer nicely non-formatted strings.
1469 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1470 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1471
1472
1473def main(argv):
1474 """Doesn't parse the arguments here, just find the right subcommand to
1475 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001476 if sys.hexversion < 0x02060000:
1477 print >> sys.stderr, (
1478 '\nYour python version %s is unsupported, please upgrade.\n' %
1479 sys.version.split(' ', 1)[0])
1480 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001481 # Reload settings.
1482 global settings
1483 settings = Settings()
1484
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485 # Do it late so all commands are listed.
1486 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1487 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1488 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1489
1490 # Create the option parse and add --verbose support.
1491 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001492 parser.add_option(
1493 '-v', '--verbose', action='count', default=0,
1494 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 old_parser_args = parser.parse_args
1496 def Parse(args):
1497 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001498 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001499 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001500 elif options.verbose:
1501 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502 else:
1503 logging.basicConfig(level=logging.WARNING)
1504 return options, args
1505 parser.parse_args = Parse
1506
1507 if argv:
1508 command = Command(argv[0])
1509 if command:
1510 # "fix" the usage and the description now that we know the subcommand.
1511 GenUsage(parser, argv[0])
1512 try:
1513 return command(parser, argv[1:])
1514 except urllib2.HTTPError, e:
1515 if e.code != 500:
1516 raise
1517 DieWithError(
1518 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1519 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1520
1521 # Not a known command. Default to help.
1522 GenUsage(parser, 'help')
1523 return CMDhelp(parser, argv)
1524
1525
1526if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001527 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001528 sys.exit(main(sys.argv[1:]))